Repository: slack-go/slack
Pull request #1211 (2023-07-10 01:54:08)

chore: slice replace loop
##### Pull Request Guidelines

These are recommendations for pull requests.
They are strictly guidelines to help manage expectations.

##### PR preparation
Run `make pr-prep` from the root of the repository to run formatting, linting and tests.

##### Should this be an issue instead
- [ ] is it a convenience method? (no new functionality, streamlines some use case)
- [ ] exposes a previously private type, const, method, etc.
- [ ] is it application specific (caching, retry logic, rate limiting, etc)
- [ ] is it performance related.

##### API changes

Since API changes have to be maintained they undergo a more detailed review and are more likely to require changes.

- no tests, if you're adding to the API include at least a single test of the happy case.
- If you can accomplish your goal without changing the API, then do so.
- dependency changes. updates are okay. adding/removing need justification.

###### Examples of API changes that do not meet guidelines:
- in library cache for users. caches are use case specific.
- Convenience methods for Sending Messages, update, post, ephemeral, etc. consider opening an issue instead.



Patch:
diff --git a/.gitignore b/.gitignore
index ac6f3ee..027f4de 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 *.test
 *~
 .idea/
+/vendor/
\ No newline at end of file
diff --git a/block.go b/block.go
index 18222bd..1d6ba2d 100644
--- a/block.go
+++ b/block.go
@@ -18,6 +18,7 @@ const (
 	MBTInput    MessageBlockType = "input"
 	MBTHeader   MessageBlockType = "header"
 	MBTRichText MessageBlockType = "rich_text"
+	MBTVideo    MessageBlockType = "video"
 )
 
 // Block defines an interface all block types should implement
@@ -39,6 +40,7 @@ type BlockAction struct {
 	Type                  ActionType          `json:"type"`
 	Text                  TextBlockObject     `json:"text"`
 	Value                 string              `json:"value"`
+	Files                 []File              `json:"files"`
 	ActionTs              string              `json:"action_ts"`
 	SelectedOption        OptionBlockObject   `json:"selected_option"`
 	SelectedOptions       []OptionBlockObject `json:"selected_options"`
diff --git a/block_conv.go b/block_conv.go
index 4ab58de..7570be2 100644
--- a/block_conv.go
+++ b/block_conv.go
@@ -2,7 +2,6 @@ package slack
 
 import (
 	"encoding/json"
-	"errors"
 	"fmt"
 )
 
@@ -66,8 +65,12 @@ func (b *Blocks) UnmarshalJSON(data []byte) error {
 			block = &InputBlock{}
 		case "rich_text":
 			block = &RichTextBlock{}
+		case "rich_text_input":
+			block = &RichTextBlock{}
 		case "section":
 			block = &SectionBlock{}
+		case "video":
+			block = &VideoBlock{}
 		default:
 			block = &UnknownBlock{}
 		}
@@ -114,6 +117,8 @@ func (b *InputBlock) UnmarshalJSON(data []byte) error {
 		e = &DateTimePickerBlockElement{}
 	case "plain_text_input":
 		e = &PlainTextInputBlockElement{}
+	case "rich_text_input":
+		e = &RichTextInputBlockElement{}
 	case "email_text_input":
 		e = &EmailTextInputBlockElement{}
 	case "url_text_input":
@@ -130,8 +135,10 @@ func (b *InputBlock) UnmarshalJSON(data []byte) error {
 		e = &RadioButtonsBlockElement{}
 	case "number_input":
 		e = &NumberInputBlockElement{}
+	case "file_input":
+		e = &FileInputBlockElement{}
 	default:
-		return errors.New("unsupported block element type")
+		return fmt.Errorf("unsupported block element type %v", s.TypeVal)
 	}
 
 	if err := json.Unmarshal(a.Element, e); err != nil {
@@ -196,6 +203,8 @@ func (b *BlockElements) UnmarshalJSON(data []byte) error {
 			blockElement = &DateTimePickerBlockElement{}
 		case "plain_text_input":
 			blockElement = &PlainTextInputBlockElement{}
+		case "rich_text_input":
+			blockElement = &RichTextInputBlockElement{}
 		case "email_text_input":
 			blockElement = &EmailTextInputBlockElement{}
 		case "url_text_input":
@@ -298,6 +307,12 @@ func (a *Accessory) UnmarshalJSON(data []byte) error {
 			return err
 		}
 		a.PlainTextInputElement = element.(*PlainTextInputBlockElement)
+	case "rich_text_input":
+		element, err := unmarshalBlockElement(r, &RichTextInputBlockElement{})
+		if err != nil {
+			return err
+		}
+		a.RichTextInputElement = element.(*RichTextInputBlockElement)
 	case "radio_buttons":
 		element, err := unmarshalBlockElement(r, &RadioButtonsBlockElement{})
 		if err != nil {
@@ -429,7 +444,7 @@ func (e *ContextElements) UnmarshalJSON(data []byte) error {
 
 			e.Elements = append(e.Elements, elem.(*ImageBlockElement))
 		default:
-			return errors.New("unsupported context element type")
+			return fmt.Errorf("unsupported context element type %v", contextElementType)
 		}
 	}
 
diff --git a/block_element.go b/block_element.go
index a70d8f2..a2b755b 100644
--- a/block_element.go
+++ b/block_element.go
@@ -12,9 +12,11 @@ const (
 	METDatetimepicker MessageElementType = "datetimepicker"
 	METPlainTextInput MessageElementType = "plain_text_input"
 	METRadioButtons   MessageElementType = "radio_buttons"
+	METRichTextInput  MessageElementType = "rich_text_input"
 	METEmailTextInput MessageElementType = "email_text_input"
 	METURLTextInput   MessageElementType = "url_text_input"
 	METNumber         MessageElementType = "number_input"
+	METFileInput      MessageElementType = "file_input"
 
 	MixedElementImage MixedElementType = "mixed_image"
 	MixedElementText  MixedElementType = "mixed_text"
@@ -51,6 +53,7 @@ type Accessory struct {
 	DatePickerElement          *DatePickerBlockElement
 	TimePickerElement          *TimePickerBlockElement
 	PlainTextInputElement      *PlainTextInputBlockElement
+	RichTextInputElement       *RichTextInputBlockElement
 	RadioButtonsElement        *RadioButtonsBlockElement
 	SelectElement              *SelectBlockElement
 	MultiSelectElement         *MultiSelectBlockElement
@@ -73,6 +76,8 @@ func NewAccessory(element BlockElement) *Accessory {
 		return &Accessory{TimePickerElement: element.(*TimePickerBlockElement)}
 	case *PlainTextInputBlockElement:
 		return &Accessory{PlainTextInputElement: element.(*PlainTextInputBlockElement)}
+	case *RichTextInputBlockElement:
+		return &Accessory{RichTextInputElement: element.(*RichTextInputBlockElement)}
 	case *RadioButtonsBlockElement:
 		return &Accessory{RadioButtonsElement: element.(*RadioButtonsBlockElement)}
 	case *SelectBlockElement:
@@ -177,6 +182,12 @@ func (s *ButtonBlockElement) WithConfirm(confirm *ConfirmationBlockObject) *Butt
 	return s
 }
 
+// WithURL adds a URL for the button to link to and returns the modified ButtonBlockElement
+func (s *ButtonBlockElement) WithURL(url string) *ButtonBlockElement {
+	s.URL = url
+	return s
+}
+
 // NewButtonBlockElement returns an instance of a new button element to be used within a block
 func NewButtonBlockElement(actionID, value string, text *TextBlockObject) *ButtonBlockElement {
 	return &ButtonBlockElement{
@@ -509,6 +520,32 @@ func NewPlainTextInputBlockElement(placeholder *TextBlockObject, actionID string
 	}
 }
 
+// RichTextInputBlockElement creates a field where allows users to enter formatted text
+// in a WYSIWYG composer, offering the same messaging writing experience as in Slack
+// More Information: https://api.slack.com/reference/block-kit/block-elements#rich_text_input
+type RichTextInputBlockElement struct {
+	Type                 MessageElementType    `json:"type"`
+	ActionID             string                `json:"action_id,omitempty"`
+	Placeholder          *TextBlockObject      `json:"placeholder,omitempty"`
+	InitialValue         string                `json:"initial_value,omitempty"`
+	DispatchActionConfig *DispatchActionConfig `json:"dispatch_action_config,omitempty"`
+	FocusOnLoad          bool                  `json:"focus_on_load,omitempty"`
+}
+
+// ElementType returns the type of the Element
+func (s RichTextInputBlockElement) ElementType() MessageElementType {
+	return s.Type
+}
+
+// NewRichTextInputBlockElement returns an instance of a rich-text input element
+func NewRichTextInputBlockElement(placeholder *TextBlockObject, actionID string) *RichTextInputBlockElement {
+	return &RichTextInputBlockElement{
+		Type:        METRichTextInput,
+		ActionID:    actionID,
+		Placeholder: placeholder,
+	}
+}
+
 // CheckboxGroupsBlockElement defines an element which allows users to choose
 // one or more items from a list of possible options.
 //
@@ -591,3 +628,40 @@ func NewNumberInputBlockElement(placeholder *TextBlockObject, actionID string, i
 		IsDecimalAllowed: isDecimalAllowed,
 	}
 }
+
+// FileInputBlockElement creates a field where a user can upload a file.
+//
+// File input elements are currently only available in modals.
+//
+// More Information: https://api.slack.com/reference/block-kit/block-elements#file_input
+type FileInputBlockElement struct {
+	Type      MessageElementType `json:"type"`
+	ActionID  string             `json:"action_id,omitempty"`
+	FileTypes []string           `json:"filetypes,omitempty"`
+	MaxFiles  int                `json:"max_files,omitempty"`
+}
+
+// ElementType returns the type of the Element
+func (s FileInputBlockElement) ElementType() MessageElementType {
+	return s.Type
+}
+
+// NewFileInputBlockElement returns an instance of a file input element
+func NewFileInputBlockElement(actionID string) *FileInputBlockElement {
+	return &FileInputBlockElement{
+		Type:     METFileInput,
+		ActionID: actionID,
+	}
+}
+
+// WithFileTypes sets the file types that can be uploaded
+func (s *FileInputBlockElement) WithFileTypes(fileTypes ...string) *FileInputBlockElement {
+	s.FileTypes = fileTypes
+	return s
+}
+
+// WithMaxFiles sets the maximum number of files that can be uploaded
+func (s *FileInputBlockElement) WithMaxFiles(maxFiles int) *FileInputBlockElement {
+	s.MaxFiles = maxFiles
+	return s
+}
diff --git a/block_object.go b/block_object.go
index f70405e..a3e78d4 100644
--- a/block_object.go
+++ b/block_object.go
@@ -147,6 +147,16 @@ func (s TextBlockObject) Validate() error {
 		return errors.New("emoji cannot be true in mrkdown")
 	}
 
+	// https://api.slack.com/reference/block-kit/composition-objects#text__fields
+	if len(s.Text) == 0 {
+		return errors.New("text must have a minimum length of 1")
+	}
+
+	// https://api.slack.com/reference/block-kit/composition-objects#text__fields
+	if len(s.Text) > 3000 {
+		return errors.New("text cannot be longer than 3000 characters")
+	}
+
 	return nil
 }
 
@@ -177,7 +187,7 @@ type ConfirmationBlockObject struct {
 	Title   *TextBlockObject `json:"title"`
 	Text    *TextBlockObject `json:"text"`
 	Confirm *TextBlockObject `json:"confirm"`
-	Deny    *TextBlockObject `json:"deny"`
+	Deny    *TextBlockObject `json:"deny,omitempty"`
 	Style   Style            `json:"style,omitempty"`
 }
 
diff --git a/block_rich_text.go b/block_rich_text.go
index 01e5cdb..b6a4b4c 100644
--- a/block_rich_text.go
+++ b/block_rich_text.go
@@ -40,6 +40,12 @@ func (e *RichTextBlock) UnmarshalJSON(b []byte) error {
 		switch s.Type {
 		case RTESection:
 			elem = &RichTextSection{}
+		case RTEList:
+			elem = &RichTextList{}
+		case RTEQuote:
+			elem = &RichTextQuote{}
+		case RTEPreformatted:
+			elem = &RichTextPreformatted{}
 		default:
 			elems = append(elems, &RichTextUnknown{
 				Type: s.Type,
@@ -92,12 +98,92 @@ func (u RichTextUnknown) RichTextElementType() RichTextElementType {
 	return u.Type
 }
 
+type RichTextListElementType string
+
+const (
+	RTEListOrdered RichTextListElementType = "ordered"
+	RTEListBullet  RichTextListElementType = "bullet"
+)
+
+type RichTextList struct {
+	Type     RichTextElementType     `json:"type"`
+	Elements []RichTextElement       `json:"elements"`
+	Style    RichTextListElementType `json:"style"`
+	Indent   int                     `json:"indent"`
+}
+
+// NewRichTextList returns a new rich text list element.
+func NewRichTextList(style RichTextListElementType, indent int, elements ...RichTextElement) *RichTextList {
+	return &RichTextList{
+		Type:     RTEList,
+		Elements: elements,
+		Style:    style,
+		Indent:   indent,
+	}
+}
+
+// ElementType returns the type of the Element
+func (s RichTextList) RichTextElementType() RichTextElementType {
+	return s.Type
+}
+
+func (e *RichTextList) UnmarshalJSON(b []byte) error {
+	var raw struct {
+		RawElements []json.RawMessage       `json:"elements"`
+		Style       RichTextListElementType `json:"style"`
+		Indent      int                     `json:"indent"`
+	}
+	if string(b) == "{}" {
+		return nil
+	}
+	if err := json.Unmarshal(b, &raw); err != nil {
+		return err
+	}
+	elems := make([]RichTextElement, 0, len(raw.RawElements))
+	for _, r := range raw.RawElements {
+		var s struct {
+			Type RichTextElementType `json:"type"`
+		}
+		if err := json.Unmarshal(r, &s); err != nil {
+			return err
+		}
+		var elem RichTextElement
+		switch s.Type {
+		case RTESection:
+			elem = &RichTextSection{}
+		case RTEList:
+			elem = &RichTextList{}
+		case RTEQuote:
+			elem = &RichTextQuote{}
+		case RTEPreformatted:
+			elem = &RichTextPreformatted{}
+		default:
+			elems = append(elems, &RichTextUnknown{
+				Type: s.Type,
+				Raw:  string(r),
+			})
+			continue
+		}
+		if err := json.Unmarshal(r, elem); err != nil {
+			return err
+		}
+		elems = append(elems, elem)
+	}
+	*e = RichTextList{
+		Type:     RTEList,
+		Elements: elems,
+		Style:    raw.Style,
+		Indent:   raw.Indent,
+	}
+	return nil
+}
+
 type RichTextSection struct {
 	Type     RichTextElementType      `json:"type"`
 	Elements []RichTextSectionElement `json:"elements"`
 }
 
-// ElementType returns the type of the Element
+// RichTextElementType returns the type of the Element
 func (s RichTextSection) RichTextElementType() RichTextElementType {
 	return s.Type
 }
@@ -381,3 +467,62 @@ type RichTextSectionUnknownElement struct {
 func (r RichTextSectionUnknownElement) RichTextSectionElementType() RichTextSectionElementType {
 	return r.Type
 }
+
+// RichTextQuote represents rich_text_quote element type.
+type RichTextQuote RichTextSection
+
+// RichTextElementType returns the type of the Element
+func (s *RichTextQuote) RichTextElementType() RichTextElementType {
+	return s.Type
+}
+
+func (s *RichTextQuote) UnmarshalJSON(b []byte) error {
+	// reusing the RichTextSection struct, as it's the same as RichTextQuote.
+	var rts RichTextSection
+	if err := json.Unmarshal(b, &rts); err != nil {
+		return err
+	}
+	*s = RichTextQuote(rts)
+	s.Type = RTEQuote
+	return nil
+}
+
+// RichTextPreformatted represents rich_text_quote element type.
+type RichTextPreformatted struct {
+	RichTextSection
+	Border int `json:"border"`
+}
+
+// RichTextElementType returns the type of the Element
+func (s *RichTextPreformatted) RichTextElementType() RichTextElementType {
+	return s.Type
+}
+
+func (s *RichTextPreformatted) UnmarshalJSON(b []byte) error {
+	var rts RichTextSection
+	if err := json.Unmarshal(b, &rts); err != nil {
+		return err
+	}
+	// we define standalone fields because we need to unmarshal the border
+	// field.  We can not directly unmarshal the data into
+	// RichTextPreformatted because it will cause an infinite loop.  We also
+	// can not define a struct with embedded RichTextSection and Border fields
+	// because the json package will not unmarshal the data into the
+	// standalone fields, once it sees UnmarshalJSON method on the embedded
+	// struct.  The drawback is that we have to process the data twice, and
+	// have to define a standalone struct with the same set of fields as the
+	// original struct, which may become a maintenance burden (i.e. update the
+	// fields in two places, should it ever change).
+	var standalone struct {
+		Border int `json:"border"`
+	}
+	if err := json.Unmarshal(b, &standalone); err != nil {
+		return err
+	}
+	*s = RichTextPreformatted{
+		RichTextSection: rts,
+		Border:          standalone.Border,
+	}
+	s.Type = RTEPreformatted
+	return nil
+}
diff --git a/block_video.go b/block_video.go
new file mode 100644
index 0000000..322c614
--- /dev/null
+++ b/block_video.go
@@ -0,0 +1,65 @@
+package slack
+
+// VideoBlock defines data required to display a video as a block element
+//
+// More Information: https://api.slack.com/reference/block-kit/blocks#video
+type VideoBlock struct {
+	Type            MessageBlockType `json:"type"`
+	VideoURL        string           `json:"video_url"`
+	ThumbnailURL    string           `json:"thumbnail_url"`
+	AltText         string           `json:"alt_text"`
+	Title           *TextBlockObject `json:"title"`
+	BlockID         string           `json:"block_id,omitempty"`
+	TitleURL        string           `json:"title_url,omitempty"`
+	AuthorName      string           `json:"author_name,omitempty"`
+	ProviderName    string           `json:"provider_name,omitempty"`
+	ProviderIconURL string           `json:"provider_icon_url,omitempty"`
+	Description     *TextBlockObject `json:"description,omitempty"`
+}
+
+// BlockType returns the type of the block
+func (s VideoBlock) BlockType() MessageBlockType {
+	return s.Type
+}
+
+// NewVideoBlock returns an instance of a new Video Block type
+func NewVideoBlock(videoURL, thumbnailURL, altText, blockID string, title *TextBlockObject) *VideoBlock {
+	return &VideoBlock{
+		Type:         MBTVideo,
+		VideoURL:     videoURL,
+		ThumbnailURL: thumbnailURL,
+		AltText:      altText,
+		BlockID:      blockID,
+		Title:        title,
+	}
+}
+
+// WithAuthorName sets the author name for the VideoBlock
+func (s *VideoBlock) WithAuthorName(authorName string) *VideoBlock {
+	s.AuthorName = authorName
+	return s
+}
+
+// WithTitleURL sets the title URL for the VideoBlock
+func (s *VideoBlock) WithTitleURL(titleURL string) *VideoBlock {
+	s.TitleURL = titleURL
+	return s
+}
+
+// WithDescription sets the description for the VideoBlock
+func (s *VideoBlock) WithDescription(description *TextBlockObject) *VideoBlock {
+	s.Description = description
+	return s
+}
+
+// WithProviderIconURL sets the provider icon URL for the VideoBlock
+func (s *VideoBlock) WithProviderIconURL(providerIconURL string) *VideoBlock {
+	s.ProviderIconURL = providerIconURL
+	return s
+}
+
+// WithProviderName sets the provider name for the VideoBlock
+func (s *VideoBlock) WithProviderName(providerName string) *VideoBlock {
+	s.ProviderName = providerName
+	return s
+}
diff --git a/bots.go b/bots.go
index da21ba0..fbe5d25 100644
--- a/bots.go
+++ b/bots.go
@@ -35,19 +35,28 @@ func (api *Client) botRequest(ctx context.Context, path string, values url.Value
 	return response, nil
 }
 
+type GetBotInfoParameters struct {
+	Bot    string
+	TeamID string
+}
+
 // GetBotInfo will retrieve the complete bot information
-func (api *Client) GetBotInfo(bot string) (*Bot, error) {
-	return api.GetBotInfoContext(context.Background(), bot)
+func (api *Client) GetBotInfo(parameters GetBotInfoParameters) (*Bot, error) {
+	return api.GetBotInfoContext(context.Background(), parameters)
 }
 
 // GetBotInfoContext will retrieve the complete bot information using a custom context
-func (api *Client) GetBotInfoContext(ctx context.Context, bot string) (*Bot, error) {
+func (api *Client) GetBotInfoContext(ctx context.Context, parameters GetBotInfoParameters) (*Bot, error) {
 	values := url.Values{
 		"token": {api.token},
 	}
 
-	if bot != "" {
-		values.Add("bot", bot)
+	if parameters.Bot != "" {
+		values.Add("bot", parameters.Bot)
+	}
+
+	if parameters.TeamID != "" {
+		values.Add("team_id", parameters.TeamID)
 	}
 
 	response, err := api.botRequest(ctx, "bots.info", values)
diff --git a/chat.go b/chat.go
index 4448e9f..d043ef0 100644
--- a/chat.go
+++ b/chat.go
@@ -7,6 +7,7 @@ import (
 	"io/ioutil"
 	"net/http"
 	"net/url"
+	"regexp"
 	"strconv"
 
 	"github.com/slack-go/slack/slackutilsx"
@@ -29,9 +30,9 @@ const (
 
 type chatResponseFull struct {
 	Channel            string `json:"channel"`
-	Timestamp          string `json:"ts"`                             //Regular message timestamp
-	MessageTimeStamp   string `json:"message_ts"`                     //Ephemeral message timestamp
-	ScheduledMessageID string `json:"scheduled_message_id,omitempty"` //Scheduled message id
+	Timestamp          string `json:"ts"`                             // Regular message timestamp
+	MessageTimeStamp   string `json:"message_ts"`                     // Ephemeral message timestamp
+	ScheduledMessageID string `json:"scheduled_message_id,omitempty"` // Scheduled message id
 	Text               string `json:"text"`
 	SlackResponse
 }
@@ -224,7 +225,7 @@ func (api *Client) SendMessageContext(ctx context.Context, channelID string, opt
 			return "", "", "", err
 		}
 		req.Body = ioutil.NopCloser(bytes.NewBuffer(reqBody))
-		api.Debugf("Sending request: %s", string(reqBody))
+		api.Debugf("Sending request: %s", redactToken(reqBody))
 	}
 
 	if err = doPost(ctx, api.httpclient, req, parser(&response), api); err != nil {
@@ -234,6 +235,20 @@ func (api *Client) SendMessageContext(ctx context.Context, channelID string, opt
 	return response.Channel, response.getMessageTimestamp(), response.Text, response.Err()
 }
 
+func redactToken(b []byte) []byte {
+	// See https://api.slack.com/authentication/token-types
+	// and https://api.slack.com/authentication/rotation
+	re, err := regexp.Compile(`(token=x[a-z.]+)-[0-9A-Za-z-]+`)
+	if err != nil {
+		// The regular expression above should never result in errors,
+		// but just in case, do no harm.
+		return b
+	}
+	// Keep "token=" and the first element of the token, which identifies its type
+	// (this could be useful for debugging, e.g. when using a wrong token).
+	return re.ReplaceAll(b, []byte("$1-REDACTED"))
+}
+
 // UnsafeApplyMsgOptions utility function for debugging/testing chat requests.
 // NOTE: USE AT YOUR OWN RISK: No issues relating to the use of this function
 // will be supported by the library.
@@ -681,6 +696,14 @@ func MsgOptionMetadata(metadata SlackMetadata) MsgOption {
 	}
 }
 
+// MsgOptionLinkNames finds and links user groups. Does not support linking individual users
+func MsgOptionLinkNames(linkName bool) MsgOption {
+	return func(config *sendConfig) error {
+		config.values.Set("link_names", strconv.FormatBool(linkName))
+		return nil
+	}
+}
+
 // UnsafeMsgOptionEndpoint deliver the message to the specified endpoint.
 // NOTE: USE AT YOUR OWN RISK: No issues relating to the use of this Option
 // will be supported by the library, it is subject to change without notice that
@@ -784,6 +807,7 @@ func (api *Client) GetPermalinkContext(ctx context.Context, params *PermalinkPar
 
 type GetScheduledMessagesParameters struct {
 	Channel string
+	TeamID  string
 	Cursor  string
 	Latest  string
 	Limit   int
@@ -803,6 +827,9 @@ func (api *Client) GetScheduledMessagesContext(ctx context.Context, params *GetS
 	if params.Channel != "" {
 		values.Add("channel", params.Channel)
 	}
+	if params.TeamID != "" {
+		values.Add("team_id", params.TeamID)
+	}
 	if params.Cursor != "" {
 		values.Add("cursor", params.Cursor)
 	}
diff --git a/conversation.go b/conversation.go
index 44f06ea..787a961 100644
--- a/conversation.go
+++ b/conversation.go
@@ -22,6 +22,7 @@ type Conversation struct {
 	IsIM               bool     `json:"is_im"`
 	IsExtShared        bool     `json:"is_ext_shared"`
 	IsOrgShared        bool     `json:"is_org_shared"`
+	IsGlobalShared     bool     `json:"is_global_shared"`
 	IsPendingExtShared bool     `json:"is_pending_ext_shared"`
 	IsPrivate          bool     `json:"is_private"`
 	IsMpIM             bool     `json:"is_mpim"`
@@ -30,6 +31,9 @@ type Conversation struct {
 	NumMembers         int      `json:"num_members"`
 	Priority           float64  `json:"priority"`
 	User               string   `json:"user"`
+	ConnectedTeamIDs   []string `json:"connected_team_ids,omitempty"`
+	SharedTeamIDs      []string `json:"shared_team_ids,omitempty"`
+	InternalTeamIDs    []string `json:"internal_team_ids,omitempty"`
 
 	// TODO support pending_shared
 	// TODO support previous_names
@@ -296,6 +300,53 @@ func (api *Client) InviteUsersToConversationContext(ctx context.Context, channel
 	return response.Channel, response.Err()
 }
 
+// InviteSharedEmailsToConversation invites users to a shared channels by email
+func (api *Client) InviteSharedEmailsToConversation(channelID string, emails ...string) (string, bool, error) {
+	return api.inviteSharedToConversationHelper(context.Background(), channelID, emails, nil)
+}
+
+// InviteSharedEmailsToConversationContext invites users to a shared channels by email using context
+func (api *Client) InviteSharedEmailsToConversationContext(ctx context.Context, channelID string, emails ...string) (string, bool, error) {
+	return api.inviteSharedToConversationHelper(ctx, channelID, emails, nil)
+}
+
+// InviteSharedUserIDsToConversation invites users to a shared channels by user id
+func (api *Client) InviteSharedUserIDsToConversation(channelID string, userIDs ...string) (string, bool, error) {
+	return api.inviteSharedToConversationHelper(context.Background(), channelID, nil, userIDs)
+}
+
+// InviteSharedUserIDsToConversationContext invites users to a shared channels by user id with context
+func (api *Client) InviteSharedUserIDsToConversationContext(ctx context.Context, channelID string, userIDs ...string) (string, bool, error) {
+	return api.inviteSharedToConversationHelper(ctx, channelID, nil, userIDs)
+}
+
+// inviteSharedToConversationHelper invites emails or userIDs to a channel with a custom context.
+// This is a helper function for InviteSharedEmailsToConversation and InviteSharedUserIDsToConversation.
+// It accepts either emails or userIDs, but not both.
+func (api *Client) inviteSharedToConversationHelper(ctx context.Context, channelID string, emails []string, userIDs []string) (string, bool, error) {
+	values := url.Values{
+		"token":   {api.token},
+		"channel": {channelID},
+	}
+	if len(emails) > 0 {
+		values.Add("emails", strings.Join(emails, ","))
+	} else if len(userIDs) > 0 {
+		values.Add("user_ids", strings.Join(userIDs, ","))
+	}
+	response := struct {
+		SlackResponse
+		InviteID              string `json:"invite_id"`
+		IsLegacySharedChannel bool   `json:"is_legacy_shared_channel"`
+	}{}
+
+	err := api.postMethod(ctx, "conversations.inviteShared", values, &response)
+	if err != nil {
+		return "", false, err
+	}
+
+	return response.InviteID, response.IsLegacySharedChannel, response.Err()
+}
+
 // KickUserFromConversation removes a user from a conversation
 func (api *Client) KickUserFromConversation(channelID string, user string) error {
 	return api.KickUserFromConversationContext(context.Background(), channelID, user)
diff --git a/examples/blocks/README.md b/examples/blocks/README.md
index 588f248..2e5d81f 100644
--- a/examples/blocks/README.md
+++ b/examples/blocks/README.md
@@ -53,7 +53,7 @@ To preview this block on the builder website, you should copy just the contents
 The first example demonstrates usage of Sections, Fields and Action buttons.  You can view the [Approval Example](https://api.slack.com/tools/block-kit-builder?blocks=%5B%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22You%20have%20a%20new%20request%3A%5Cn*%3CfakeLink.toEmployeeProfile.com%7CFred%20Enriquez%20-%20New%20device%20request%3E*%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22fields%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22*Type%3A*%5CnComputer%20(laptop)%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22*When%3A*%5CnSubmitted%20Aut%2010%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22*Last%20Update%3A*%5CnMar%2010%2C%202015%20(3%20years%2C%205%20months)%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22*Reason%3A*%5CnAll%20vowel%20keys%20aren%27t%20working.%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22*Specs%3A*%5Cn%5C%22Cheetah%20Pro%2015%5C%22%20-%20Fast%2C%20really%20fast%5C%22%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22actions%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Approve%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Deny%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%0A%5D) on the block kit builder website. This example can be generated with the function named `exampleOne`.
 
 #### Example 2 - Approval - With Images
-The secoond example adds additional complexity by introducing images as accessories to main blocks of text. You can view this [Approval Example with Images](https://api.slack.com/tools/block-kit-builder?blocks=%5B%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22You%20have%20a%20new%20request%3A%5Cn*%3Cgoogle.com%7CFred%20Enriquez%20-%20Time%20Off%20request%3E*%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*Type%3A*%5CnPaid%20time%20off%5Cn*When%3A*%5CnAug%2010-Aug%2013%5Cn*Hours%3A*%2016.0%20(2%20days)%5Cn*Remaining%20balance%3A*%2032.0%20hours%20(4%20days)%5Cn*Comments%3A*%20%5C%22Family%20in%20town%2C%20going%20camping!%5C%22%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2FapprovalsNewDevice.png%22%2C%0A%09%09%09%22alt_text%22%3A%20%22computer%20thumbnail%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22actions%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Approve%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Deny%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%0A%5D) on the block kit builder website. This example can be generated with the function named `exampleTwo`.
+The second example adds additional complexity by introducing images as accessories to main blocks of text. You can view this [Approval Example with Images](https://api.slack.com/tools/block-kit-builder?blocks=%5B%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22You%20have%20a%20new%20request%3A%5Cn*%3Cgoogle.com%7CFred%20Enriquez%20-%20Time%20Off%20request%3E*%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*Type%3A*%5CnPaid%20time%20off%5Cn*When%3A*%5CnAug%2010-Aug%2013%5Cn*Hours%3A*%2016.0%20(2%20days)%5Cn*Remaining%20balance%3A*%2032.0%20hours%20(4%20days)%5Cn*Comments%3A*%20%5C%22Family%20in%20town%2C%20going%20camping!%5C%22%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2FapprovalsNewDevice.png%22%2C%0A%09%09%09%22alt_text%22%3A%20%22computer%20thumbnail%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22actions%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Approve%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Deny%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%0A%5D) on the block kit builder website. This example can be generated with the function named `exampleTwo`.
 
 #### Example 3 - Notifications
 This example shows how to add actions to your block that will trigger an interactive message to your application.  You can view the rendered example for [Notifications](https://api.slack.com/tools/block-kit-builder?blocks=%5B%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%22text%22%3A%20%22Looks%20like%20you%20have%20a%20scheduling%20conflict%20with%20this%20event%3A%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3CfakeLink.toUserProfiles.com%7CIris%20%2F%20Zelda%201-1%3E*%5CnTuesday%2C%20January%2021%204%3A00-4%3A30pm%5CnBuilding%202%20-%20Havarti%20Cheese%20(3)%5Cn2%20guests%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2Fnotifications.png%22%2C%0A%09%09%09%22alt_text%22%3A%20%22calendar%20thumbnail%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22context%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2FnotificationsWarningIcon.png%22%2C%0A%09%09%09%09%22alt_text%22%3A%20%22notifications%20warning%20icon%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22*Conflicts%20with%20Team%20Huddle%3A%204%3A15-4%3A30pm*%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*Propose%20a%20new%20time%3A*%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*Today%20-%204%3A30-5pm*%5CnEveryone%20is%20available%3A%20%40iris%2C%20%40zelda%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Choose%22%0A%09%09%09%7D%2C%0A%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*Tomorrow%20-%204-4%3A30pm*%5CnEveryone%20is%20available%3A%20%40iris%2C%20%40zelda%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Choose%22%0A%09%09%09%7D%2C%0A%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*Tomorrow%20-%206-6%3A30pm*%5CnSome%20people%20aren%27t%20available%3A%20%40iris%2C%20~%40zelda~%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Choose%22%0A%09%09%09%7D%2C%0A%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3Cfakelink.ToMoreTimes.com%7CShow%20more%20times%3E*%22%0A%09%09%7D%0A%09%7D%0A%5D) on the block builder website. Refer to the function `exampleThree` for details on how this block can be generated.
diff --git a/examples/buttons/buttons.go b/examples/buttons/buttons.go
index 82772fa..b5ef09c 100644
--- a/examples/buttons/buttons.go
+++ b/examples/buttons/buttons.go
@@ -50,7 +50,7 @@ func main() {
 	if err != nil {
 		fmt.Printf("Could not send message: %v", err)
 	}
-	fmt.Printf("Message with buttons sucessfully sent to channel %s at %s", channelID, timestamp)
+	fmt.Printf("Message with buttons successfully sent to channel %s at %s", channelID, timestamp)
 	http.HandleFunc("/actions", actionHandler)
 	http.ListenAndServe(":3000", nil)
 }
diff --git a/examples/manifests/README.md b/examples/manifests/README.md
new file mode 100644
index 0000000..ba6db23
--- /dev/null
+++ b/examples/manifests/README.md
@@ -0,0 +1,38 @@
+# Manifest examples
+
+This example shows how to interact with the
+new [manifest endpoints](https://api.slack.com/reference/manifests#manifest_apis). These endpoints require a special set
+of tokens called `configuration tokens`. Refer to
+the [relevant documentation](https://api.slack.com/authentication/config-tokens) for how to create these tokens.
+
+For examples on how to use configuration tokens, see the [tokens example](../tokens).
+
+## Usage info
+
+The manifest endpoints allow you to configure your application programmatically instead of manually creating
+a `manifest.yaml` file and uploading it on your Slack application's dashboard.
+
+A manifest should follow a specific structure and has a handful of required fields. These are describe in
+the [manifest documentation](https://api.slack.com/reference/manifests#fields), but Slack additionally returns very
+informative error messages for malformed templates to help you pin down what the issue is. The library itself does not
+attempt to perform any form of validation on your manifest.
+
+**Note that each configuration token may only be used once before being invalidated. Again refer to the tokens example
+for more information.**
+
+## Available methods
+
+- ``Slack.CreateManifest()``
+- ``Slack.DeleteManifest()``
+- ``Slack.ExportManifest()``
+- ``Slack.UpdateManifest()``
+
+## Example details
+
+The example code here only shows how to _update_ an application using a manifest. The other available methods are either
+identical in usage or trivial to use, so no full example is provided for them.
+
+The example doesn't rotate the configuration tokens after updating the manifest. **You should almost always do this**.
+Your access token is invalidated after sending a request, and rotating your tokens will allow you to make another
+request in the future. This example does not do this explicitly as it would just repeat the tokens example. For sake of
+simplicity, it only focuses on the manifest part.
diff --git a/examples/manifests/manifest.go b/examples/manifests/manifest.go
new file mode 100644
index 0000000..bfcfa9c
--- /dev/null
+++ b/examples/manifests/manifest.go
@@ -0,0 +1,45 @@
+package manifests
+
+import (
+	"fmt"
+	"github.com/slack-go/slack"
+)
+
+// createManifest programmatically creates a Slack app manifest
+func createManifest() *slack.Manifest {
+	return &slack.Manifest{
+		Display: slack.Display{
+			Name: "Your Application",
+		},
+		// ... other configuration here
+	}
+}
+
+func main() {
+	api := slack.New(
+		"YOUR_TOKEN_HERE",
+		// You may choose to provide your access token when creating your Slack client
+		// or when invoking the method calls
+		slack.OptionConfigToken("YOUR_CONFIG_ACCESS_TOKEN_HERE"),
+	)
+
+	// Create a new Manifest object
+	manifest := createManifest()
+
+	// Update your application using the new manifest
+	// You may pass your token as a parameter here as well, if you didn't do it above
+	response, err := api.UpdateManifest(manifest, "", "YOUR_APP_ID_HERE")
+	if err != nil {
+		fmt.Printf("error updating Slack application: %v\n", err)
+		return
+	}
+
+	if !response.Ok {
+		fmt.Printf("unable to update Slack application: %v\n", response.Errors)
+	}
+
+	fmt.Println("successfully updated Slack application")
+
+	// The access token is now invalid, so it should be rotated for future use
+	// Refer to the examples about tokens for more details
+}
diff --git a/examples/modal/modal.go b/examples/modal/modal.go
index e628e85..155f2f7 100644
--- a/examples/modal/modal.go
+++ b/examples/modal/modal.go
@@ -19,6 +19,7 @@ import (
 	"net/http"
 
 	"github.com/slack-go/slack"
+	"time"
 )
 
 func generateModalRequest() slack.ModalViewRequest {
@@ -60,6 +61,30 @@ func generateModalRequest() slack.ModalViewRequest {
 	return modalRequest
 }
 
+func updateModal() slack.ModalViewRequest {
+	// Create a ModalViewRequest with a header and two inputs
+	titleText := slack.NewTextBlockObject("plain_text", "My App", false, false)
+	closeText := slack.NewTextBlockObject("plain_text", "Close", false, false)
+	submitText := slack.NewTextBlockObject("plain_text", "Submit", false, false)
+
+	headerText := slack.NewTextBlockObject("mrkdwn", "Modal updated!", false, false)
+	headerSection := slack.NewSectionBlock(headerText, nil, nil)
+
+	blocks := slack.Blocks{
+		BlockSet: []slack.Block{
+			headerSection,
+		},
+	}
+
+	var modalRequest slack.ModalViewRequest
+	modalRequest.Type = slack.ViewType("modal")
+	modalRequest.Title = titleText
+	modalRequest.Close = closeText
+	modalRequest.Submit = submitText
+	modalRequest.Blocks = blocks
+	return modalRequest
+}
+
 // This was taken from the slash example
 // https://github.com/slack-go/slack/blob/master/examples/slash/slash.go
 func verifySigningSecret(r *http.Request) error {
@@ -104,7 +129,7 @@ func handleSlash(w http.ResponseWriter, r *http.Request) {
 	}
 
 	switch s.Command {
-	case "/humboldttest":
+	case "/slash":
 		api := slack.New("YOUR_TOKEN_HERE")
 		modalRequest := generateModalRequest()
 		_, err = api.OpenView(s.TriggerID, modalRequest)
@@ -134,21 +159,30 @@ func handleModal(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	// Note there might be a better way to get this info, but I figured this structure out from looking at the json response
-	firstName := i.View.State.Values["First Name"]["firstName"].Value
-	lastName := i.View.State.Values["Last Name"]["lastName"].Value
-
-	msg := fmt.Sprintf("Hello %s %s, nice to meet you!", firstName, lastName)
-
 	api := slack.New("YOUR_TOKEN_HERE")
-	_, _, err = api.PostMessage(i.User.ID,
-		slack.MsgOptionText(msg, false),
-		slack.MsgOptionAttachments())
 	if err != nil {
 		fmt.Printf(err.Error())
 		w.WriteHeader(http.StatusUnauthorized)
 		return
 	}
+
+	// update modal sample
+	switch i.Type {
+	//update when interaction type is view_submission
+	case slack.InteractionTypeViewSubmission:
+		//you can use any modal you want to show to users just like creating modal.
+		updateModal := updateModal()
+		// You must set one of external_id or view_id and you can use hash for avoiding race condition.
+		// More details: https://api.slack.com/surfaces/modals/using#updating_apis
+		_, err := api.UpdateView(updateModal, "", i.View.Hash, i.View.ID)
+		// Wait for a few seconds to see result this code is necesarry due to slack server modal is going to be closed after the update
+		time.Sleep(time.Second * 2)
+		if err != nil {
+			fmt.Printf("Error updating view: %s", err)
+			w.WriteHeader(http.StatusInternalServerError)
+			return
+		}
+	}
 }
 
 func main() {
diff --git a/examples/team/team.go b/examples/team/team.go
index 8d2fcdb..cba71aa 100644
--- a/examples/team/team.go
+++ b/examples/team/team.go
@@ -9,17 +9,16 @@ import (
 func main() {
 	api := slack.New("YOUR_TOKEN_HERE")
 	//Example for single user
-	billingActive, err := api.GetBillableInfo("U023BECGF")
+	billingActive, err := api.GetBillableInfo(slack.GetBillableInfoParams{User: "U023BECGF"})
 	if err != nil {
 		fmt.Printf("%s\n", err)
 		return
 	}
 	fmt.Printf("ID: U023BECGF, BillingActive: %v\n\n\n", billingActive["U023BECGF"])
 
-	//Example for team
-	billingActiveForTeam, _ := api.GetBillableInfoForTeam()
+	//Example for team. Note: passing empty TeamID just uses the current user team.
+	billingActiveForTeam, _ := api.GetBillableInfo(slack.GetBillableInfoParams{})
 	for id, value := range billingActiveForTeam {
 		fmt.Printf("ID: %v, BillingActive: %v\n", id, value)
 	}
-
 }
diff --git a/examples/tokens/README.md b/examples/tokens/README.md
new file mode 100644
index 0000000..7e8e163
--- /dev/null
+++ b/examples/tokens/README.md
@@ -0,0 +1,10 @@
+# Tokens examples
+
+The refresh token endpoint can be used to update
+your [configuration tokenset](https://api.slack.com/authentication/config-tokens). These tokens may only be used **once
+** before being invalidated, and are only valid for up to **12 hours**.
+
+Once a token has been used, or before it expires, you can use the `RotateTokens()` method to obtain a fresh set to use
+for the next request. Depending on your use-case you may want to store these somewhere for a future run, so they are
+only returned by the method call. If you wish to update the tokens inside the active Slack client, this can be done
+using `UpdateConfigTokens()`.
diff --git a/examples/tokens/tokens.go b/examples/tokens/tokens.go
new file mode 100644
index 0000000..63d46d2
--- /dev/null
+++ b/examples/tokens/tokens.go
@@ -0,0 +1,33 @@
+package tokens
+
+import (
+	"fmt"
+	"github.com/slack-go/slack"
+)
+
+func main() {
+	api := slack.New(
+		"YOUR_TOKEN_HERE",
+		// You may choose to provide your config tokens when creating your Slack client
+		// or when invoking the method calls
+		slack.OptionConfigToken("YOUR_CONFIG_ACCESS_TOKEN_HERE"),
+		slack.OptionConfigRefreshToken("YOUR_REFRESH_TOKEN_HERE"),
+	)
+
+	// Obtain a fresh set of tokens
+	// You may pass your tokens as a parameter here as well, if you didn't do it above
+	freshTokens, err := api.RotateTokens("", "")
+	if err != nil {
+		fmt.Printf("error rotating tokens: %v\n", err)
+		return
+	}
+
+	fmt.Printf("new access token: %s\n", freshTokens.Token)
+	fmt.Printf("new refresh token: %s\n", freshTokens.RefreshToken)
+	fmt.Printf("new tokenset expires at: %d\n", freshTokens.ExpiresAt)
+
+	// Optionally: update the tokens inside the running Slack client
+	// This isn't necessary if you restart the application after storing the tokens elsewhere,
+	// or pass them as parameters to RotateTokens() explicitly
+	api.UpdateConfigTokens(freshTokens)
+}
diff --git a/files.go b/files.go
index 3562844..40a5d16 100644
--- a/files.go
+++ b/files.go
@@ -129,6 +129,7 @@ type FileUploadParameters struct {
 type GetFilesParameters struct {
 	User          string
 	Channel       string
+	TeamID        string
 	TimestampFrom JSONTime
 	TimestampTo   JSONTime
 	Types         string
@@ -142,6 +143,7 @@ type ListFilesParameters struct {
 	Limit   int
 	User    string
 	Channel string
+	TeamID  string
 	Types   string
 	Cursor  string
 }
@@ -253,12 +255,12 @@ func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count,
 	return &response.File, response.Comments, &response.Paging, nil
 }
 
-// GetFile retreives a given file from its private download URL
+// GetFile retrieves a given file from its private download URL
 func (api *Client) GetFile(downloadURL string, writer io.Writer) error {
 	return api.GetFileContext(context.Background(), downloadURL, writer)
 }
 
-// GetFileContext retreives a given file from its private download URL with a custom context
+// GetFileContext retrieves a given file from its private download URL with a custom context
 //
 // For more details, see GetFile documentation.
 func (api *Client) GetFileContext(ctx context.Context, downloadURL string, writer io.Writer) error {
@@ -281,6 +283,9 @@ func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameter
 	if params.Channel != DEFAULT_FILES_CHANNEL {
 		values.Add("channel", params.Channel)
 	}
+	if params.TeamID != "" {
+		values.Add("team_id", params.TeamID)
+	}
 	if params.TimestampFrom != DEFAULT_FILES_TS_FROM {
 		values.Add("ts_from", strconv.FormatInt(int64(params.TimestampFrom), 10))
 	}
@@ -326,6 +331,9 @@ func (api *Client) ListFilesContext(ctx context.Context, params ListFilesParamet
 	if params.Channel != DEFAULT_FILES_CHANNEL {
 		values.Add("channel", params.Channel)
 	}
+	if params.TeamID != "" {
+		values.Add("team_id", params.TeamID)
+	}
 	if params.Limit != DEFAULT_FILES_COUNT {
 		values.Add("limit", strconv.Itoa(params.Limit))
 	}
@@ -496,9 +504,8 @@ func (api *Client) getUploadURLExternal(ctx context.Context, params getUploadURL
 func (api *Client) uploadToURL(ctx context.Context, params uploadToURLParameters) (err error) {
 	values := url.Values{}
 	if params.Content != "" {
-		values.Add("content", params.Content)
-		values.Add("token", api.token)
-		err = postForm(ctx, api.httpclient, params.UploadURL, values, nil, api)
+		contentReader := strings.NewReader(params.Content)
+		err = postWithMultipartResponse(ctx, api.httpclient, params.UploadURL, params.Filename, "file", api.token, values, contentReader, nil, api)
 	} else if params.File != "" {
 		err = postLocalWithMultipartResponse(ctx, api.httpclient, params.UploadURL, params.File, "file", api.token, values, nil, api)
 	} else if params.Reader != nil {
diff --git a/interactions.go b/interactions.go
index e362caa..8c64147 100644
--- a/interactions.go
+++ b/interactions.go
@@ -33,29 +33,30 @@ const (
 
 // InteractionCallback is sent from slack when a user interactions with a button or dialog.
 type InteractionCallback struct {
-	Type            InteractionType         `json:"type"`
-	Token           string                  `json:"token"`
-	CallbackID      string                  `json:"callback_id"`
-	ResponseURL     string                  `json:"response_url"`
-	TriggerID       string                  `json:"trigger_id"`
-	ActionTs        string                  `json:"action_ts"`
-	Team            Team                    `json:"team"`
-	Channel         Channel                 `json:"channel"`
-	User            User                    `json:"user"`
-	OriginalMessage Message                 `json:"original_message"`
-	Message         Message                 `json:"message"`
-	Name            string                  `json:"name"`
-	Value           string                  `json:"value"`
-	MessageTs       string                  `json:"message_ts"`
-	AttachmentID    string                  `json:"attachment_id"`
-	ActionCallback  ActionCallbacks         `json:"actions"`
-	View            View                    `json:"view"`
-	ActionID        string                  `json:"action_id"`
-	APIAppID        string                  `json:"api_app_id"`
-	BlockID         string                  `json:"block_id"`
-	Container       Container               `json:"container"`
-	Enterprise      Enterprise              `json:"enterprise"`
-	WorkflowStep    InteractionWorkflowStep `json:"workflow_step"`
+	Type                InteractionType         `json:"type"`
+	Token               string                  `json:"token"`
+	CallbackID          string                  `json:"callback_id"`
+	ResponseURL         string                  `json:"response_url"`
+	TriggerID           string                  `json:"trigger_id"`
+	ActionTs            string                  `json:"action_ts"`
+	Team                Team                    `json:"team"`
+	Channel             Channel                 `json:"channel"`
+	User                User                    `json:"user"`
+	OriginalMessage     Message                 `json:"original_message"`
+	Message             Message                 `json:"message"`
+	Name                string                  `json:"name"`
+	Value               string                  `json:"value"`
+	MessageTs           string                  `json:"message_ts"`
+	AttachmentID        string                  `json:"attachment_id"`
+	ActionCallback      ActionCallbacks         `json:"actions"`
+	View                View                    `json:"view"`
+	ActionID            string                  `json:"action_id"`
+	APIAppID            string                  `json:"api_app_id"`
+	BlockID             string                  `json:"block_id"`
+	Container           Container               `json:"container"`
+	Enterprise          Enterprise              `json:"enterprise"`
+	IsEnterpriseInstall bool                    `json:"is_enterprise_install"`
+	WorkflowStep        InteractionWorkflowStep `json:"workflow_step"`
 	DialogSubmissionCallback
 	ViewSubmissionCallback
 	ViewClosedCallback
diff --git a/manifests.go b/manifests.go
new file mode 100644
index 0000000..0041bf2
--- /dev/null
+++ b/manifests.go
@@ -0,0 +1,287 @@
+package slack
+
+import (
+	"context"
+	"encoding/json"
+	"net/url"
+)
+
+// Manifest is an application manifest schema
+type Manifest struct {
+	Metadata    ManifestMetadata `json:"_metadata,omitempty" yaml:"_metadata,omitempty"`
+	Display     Display          `json:"display_information" yaml:"display_information"`
+	Settings    Settings         `json:"settings,omitempty" yaml:"settings,omitempty"`
+	Features    Features         `json:"features,omitempty" yaml:"features,omitempty"`
+	OAuthConfig OAuthConfig      `json:"oauth_config,omitempty" yaml:"oauth_config,omitempty"`
+}
+
+// CreateManifest creates an app from an app manifest
+func (api *Client) CreateManifest(manifest *Manifest, token string) (*ManifestResponse, error) {
+	return api.CreateManifestContext(context.Background(), manifest, token)
+}
+
+// CreateManifestContext creates an app from an app manifest with a custom context
+func (api *Client) CreateManifestContext(ctx context.Context, manifest *Manifest, token string) (*ManifestResponse, error) {
+	if token == "" {
+		token = api.configToken
+	}
+
+	jsonBytes, err := json.Marshal(manifest)
+	if err != nil {
+		return nil, err
+	}
+
+	values := url.Values{
+		"token":    {token},
+		"manifest": {string(jsonBytes)},
+	}
+
+	response := &ManifestResponse{}
+	err = api.postMethod(ctx, "apps.manifest.create", values, response)
+	if err != nil {
+		return nil, err
+	}
+
+	return response, response.Err()
+}
+
+// DeleteManifest permanently deletes an app created through app manifests
+func (api *Client) DeleteManifest(token string, appId string) (*SlackResponse, error) {
+	return api.DeleteManifestContext(context.Background(), token, appId)
+}
+
+// DeleteManifestContext permanently deletes an app created through app manifests with a custom context
+func (api *Client) DeleteManifestContext(ctx context.Context, token string, appId string) (*SlackResponse, error) {
+	if token == "" {
+		token = api.configToken
+	}
+
+	values := url.Values{
+		"token":  {token},
+		"app_id": {appId},
+	}
+
+	response := &SlackResponse{}
+	err := api.postMethod(ctx, "apps.manifest.delete", values, response)
+	if err != nil {
+		return nil, err
+	}
+
+	return response, response.Err()
+}
+
+// ExportManifest exports an app manifest from an existing app
+func (api *Client) ExportManifest(token string, appId string) (*Manifest, error) {
+	return api.ExportManifestContext(context.Background(), token, appId)
+}
+
+// ExportManifestContext exports an app manifest from an existing app with a custom context
+func (api *Client) ExportManifestContext(ctx context.Context, token string, appId string) (*Manifest, error) {
+	if token == "" {
+		token = api.configToken
+	}
+
+	values := url.Values{
+		"token":  {token},
+		"app_id": {appId},
+	}
+
+	response := &ExportManifestResponse{}
+	err := api.postMethod(ctx, "apps.manifest.export", values, response)
+	if err != nil {
+		return nil, err
+	}
+
+	return &response.Manifest, response.Err()
+}
+
+// UpdateManifest updates an app from an app manifest
+func (api *Client) UpdateManifest(manifest *Manifest, token string, appId string) (*UpdateManifestResponse, error) {
+	return api.UpdateManifestContext(context.Background(), manifest, token, appId)
+}
+
+// UpdateManifestContext updates an app from an app manifest with a custom context
+func (api *Client) UpdateManifestContext(ctx context.Context, manifest *Manifest, token string, appId string) (*UpdateManifestResponse, error) {
+	if token == "" {
+		token = api.configToken
+	}
+
+	jsonBytes, err := json.Marshal(manifest)
+	if err != nil {
+		return nil, err
+	}
+
+	values := url.Values{
+		"token":    {token},
+		"app_id":   {appId},
+		"manifest": {string(jsonBytes)},
+	}
+
+	response := &UpdateManifestResponse{}
+	err = api.postMethod(ctx, "apps.manifest.update", values, response)
+	if err != nil {
+		return nil, err
+	}
+
+	return response, response.Err()
+}
+
+// ValidateManifest sends a request to apps.manifest.validate to validate your app manifest
+func (api *Client) ValidateManifest(manifest *Manifest, token string, appId string) (*ManifestResponse, error) {
+	return api.ValidateManifestContext(context.Background(), manifest, token, appId)
+}
+
+// ValidateManifestContext sends a request to apps.manifest.validate to validate your app manifest with a custom context
+func (api *Client) ValidateManifestContext(ctx context.Context, manifest *Manifest, token string, appId string) (*ManifestResponse, error) {
+	if token == "" {
+		token = api.configToken
+	}
+
+	// Marshal manifest into string
+	jsonBytes, err := json.Marshal(manifest)
+	if err != nil {
+		return nil, err
+	}
+
+	values := url.Values{
+		"token":    {token},
+		"manifest": {string(jsonBytes)},
+	}
+
+	if appId != "" {
+		values.Add("app_id", appId)
+	}
+
+	response := &ManifestResponse{}
+	err = api.postMethod(ctx, "apps.manifest.validate", values, response)
+	if err != nil {
+		return nil, err
+	}
+
+	return response, response.Err()
+}
+
+// ManifestMetadata is a group of settings that describe the manifest
+type ManifestMetadata struct {
+	MajorVersion int `json:"major_version,omitempty" yaml:"major_version,omitempty"`
+	MinorVersion int `json:"minor_version,omitempty" yaml:"minor_version,omitempty"`
+}
+
+// Display is a group of settings that describe parts of an app's appearance within Slack
+type Display struct {
+	Name            string `json:"name" yaml:"name"`
+	Description     string `json:"description,omitempty" yaml:"description,omitempty"`
+	LongDescription string `json:"long_description,omitempty" yaml:"long_description,omitempty"`
+	BackgroundColor string `json:"background_color,omitempty" yaml:"background_color,omitempty"`
+}
+
+// Settings is a group of settings corresponding to the Settings section of the app config pages.
+type Settings struct {
+	AllowedIPAddressRanges []string           `json:"allowed_ip_address_ranges,omitempty" yaml:"allowed_ip_address_ranges,omitempty"`
+	EventSubscriptions     EventSubscriptions `json:"event_subscriptions,omitempty" yaml:"event_subscriptions,omitempty"`
+	Interactivity          Interactivity      `json:"interactivity,omitempty" yaml:"interactivity,omitempty"`
+	OrgDeployEnabled       bool               `json:"org_deploy_enabled,omitempty" yaml:"org_deploy_enabled,omitempty"`
+	SocketModeEnabled      bool               `json:"socket_mode_enabled,omitempty" yaml:"socket_mode_enabled,omitempty"`
+}
+
+// EventSubscriptions is a group of settings that describe the Events API configuration
+type EventSubscriptions struct {
+	RequestUrl string   `json:"request_url,omitempty" yaml:"request_url,omitempty"`
+	BotEvents  []string `json:"bot_events,omitempty" yaml:"bot_events,omitempty"`
+	UserEvents []string `json:"user_events,omitempty" yaml:"user_events,omitempty"`
+}
+
+// Interactivity is a group of settings that describe the interactivity configuration
+type Interactivity struct {
+	IsEnabled             bool   `json:"is_enabled" yaml:"is_enabled"`
+	RequestUrl            string `json:"request_url,omitempty" yaml:"request_url,omitempty"`
+	MessageMenuOptionsUrl string `json:"message_menu_options_url,omitempty" yaml:"message_menu_options_url,omitempty"`
+}
+
+// Features is a group of settings corresponding to the Features section of the app config pages
+type Features struct {
+	AppHome       AppHome                `json:"app_home,omitempty" yaml:"app_home,omitempty"`
+	BotUser       BotUser                `json:"bot_user,omitempty" yaml:"bot_user,omitempty"`
+	Shortcuts     []Shortcut             `json:"shortcuts,omitempty" yaml:"shortcuts,omitempty"`
+	SlashCommands []ManifestSlashCommand `json:"slash_commands,omitempty" yaml:"slash_commands,omitempty"`
+	WorkflowSteps []WorkflowStep         `json:"workflow_steps,omitempty" yaml:"workflow_steps,omitempty"`
+}
+
+// AppHome is a group of settings that describe the App Home configuration
+type AppHome struct {
+	HomeTabEnabled             bool `json:"home_tab_enabled,omitempty" yaml:"home_tab_enabled,omitempty"`
+	MessagesTabEnabled         bool `json:"messages_tab_enabled,omitempty" yaml:"messages_tab_enabled,omitempty"`
+	MessagesTabReadOnlyEnabled bool `json:"messages_tab_read_only_enabled,omitempty" yaml:"messages_tab_read_only_enabled,omitempty"`
+}
+
+// BotUser is a group of settings that describe bot user configuration
+type BotUser struct {
+	DisplayName  string `json:"display_name" yaml:"display_name"`
+	AlwaysOnline bool   `json:"always_online,omitempty" yaml:"always_online,omitempty"`
+}
+
+// Shortcut is a group of settings that describes shortcut configuration
+type Shortcut struct {
+	Name        string       `json:"name" yaml:"name"`
+	CallbackID  string       `json:"callback_id" yaml:"callback_id"`
+	Description string       `json:"description" yaml:"description"`
+	Type        ShortcutType `json:"type" yaml:"type"`
+}
+
+// ShortcutType is a new string type for the available types of shortcuts
+type ShortcutType string
+
+const (
+	MessageShortcut ShortcutType = "message"
+	GlobalShortcut  ShortcutType = "global"
+)
+
+// ManifestSlashCommand is a group of settings that describes slash command configuration
+type ManifestSlashCommand struct {
+	Command      string `json:"command" yaml:"command"`
+	Description  string `json:"description" yaml:"description"`
+	ShouldEscape bool   `json:"should_escape,omitempty" yaml:"should_escape,omitempty"`
+	Url          string `json:"url,omitempty" yaml:"url,omitempty"`
+	UsageHint    string `json:"usage_hint,omitempty" yaml:"usage_hint,omitempty"`
+}
+
+// WorkflowStep is a group of settings that describes workflow steps configuration
+type WorkflowStep struct {
+	Name       string `json:"name" yaml:"name"`
+	CallbackID string `json:"callback_id" yaml:"callback_id"`
+}
+
+// OAuthConfig is a group of settings that describe OAuth configuration for the app
+type OAuthConfig struct {
+	RedirectUrls []string    `json:"redirect_urls,omitempty" yaml:"redirect_urls,omitempty"`
+	Scopes       OAuthScopes `json:"scopes,omitempty" yaml:"scopes,omitempty"`
+}
+
+// OAuthScopes is a group of settings that describe permission scopes configuration
+type OAuthScopes struct {
+	Bot  []string `json:"bot,omitempty" yaml:"bot,omitempty"`
+	User []string `json:"user,omitempty" yaml:"user,omitempty"`
+}
+
+// ManifestResponse is the response returned by the API for apps.manifest.x endpoints
+type ManifestResponse struct {
+	Errors []ManifestValidationError `json:"errors,omitempty"`
+	SlackResponse
+}
+
+// ManifestValidationError is an error message returned for invalid manifests
+type ManifestValidationError struct {
+	Message string `json:"message"`
+	Pointer string `json:"pointer"`
+}
+
+type ExportManifestResponse struct {
+	Manifest Manifest `json:"manifest,omitempty"`
+	SlackResponse
+}
+
+type UpdateManifestResponse struct {
+	AppId              string `json:"app_id,omitempty"`
+	PermissionsUpdated bool   `json:"permissions_updated,omitempty"`
+	ManifestResponse
+}
diff --git a/oauth.go b/oauth.go
index 94b6546..ded8c73 100644
--- a/oauth.go
+++ b/oauth.go
@@ -33,17 +33,18 @@ type OAuthResponse struct {
 
 // OAuthV2Response ...
 type OAuthV2Response struct {
-	AccessToken     string                       `json:"access_token"`
-	TokenType       string                       `json:"token_type"`
-	Scope           string                       `json:"scope"`
-	BotUserID       string                       `json:"bot_user_id"`
-	AppID           string                       `json:"app_id"`
-	Team            OAuthV2ResponseTeam          `json:"team"`
-	IncomingWebhook OAuthResponseIncomingWebhook `json:"incoming_webhook"`
-	Enterprise      OAuthV2ResponseEnterprise    `json:"enterprise"`
-	AuthedUser      OAuthV2ResponseAuthedUser    `json:"authed_user"`
-	RefreshToken    string                       `json:"refresh_token"`
-	ExpiresIn       int                          `json:"expires_in"`
+	AccessToken         string                       `json:"access_token"`
+	TokenType           string                       `json:"token_type"`
+	Scope               string                       `json:"scope"`
+	BotUserID           string                       `json:"bot_user_id"`
+	AppID               string                       `json:"app_id"`
+	Team                OAuthV2ResponseTeam          `json:"team"`
+	IncomingWebhook     OAuthResponseIncomingWebhook `json:"incoming_webhook"`
+	Enterprise          OAuthV2ResponseEnterprise    `json:"enterprise"`
+	IsEnterpriseInstall bool                         `json:"is_enterprise_install"`
+	AuthedUser          OAuthV2ResponseAuthedUser    `json:"authed_user"`
+	RefreshToken        string                       `json:"refresh_token"`
+	ExpiresIn           int                          `json:"expires_in"`
 	SlackResponse
 }
 
@@ -69,6 +70,15 @@ type OAuthV2ResponseAuthedUser struct {
 	TokenType    string `json:"token_type"`
 }
 
+// OpenIDConnectResponse ...
+type OpenIDConnectResponse struct {
+	Ok          bool   `json:"ok"`
+	AccessToken string `json:"access_token"`
+	TokenType   string `json:"token_type"`
+	IdToken     string `json:"id_token"`
+	SlackResponse
+}
+
 // GetOAuthToken retrieves an AccessToken
 func GetOAuthToken(client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, err error) {
 	return GetOAuthTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI)
@@ -137,12 +147,12 @@ func GetOAuthV2ResponseContext(ctx context.Context, client httpClient, clientID,
 	return response, response.Err()
 }
 
-// RefreshOAuthV2AccessContext with a context, gets a V2 OAuth access token response
+// RefreshOAuthV2Token with a context, gets a V2 OAuth access token response
 func RefreshOAuthV2Token(client httpClient, clientID, clientSecret, refreshToken string) (resp *OAuthV2Response, err error) {
 	return RefreshOAuthV2TokenContext(context.Background(), client, clientID, clientSecret, refreshToken)
 }
 
-// RefreshOAuthV2AccessContext with a context, gets a V2 OAuth access token response
+// RefreshOAuthV2TokenContext with a context, gets a V2 OAuth access token response
 func RefreshOAuthV2TokenContext(ctx context.Context, client httpClient, clientID, clientSecret, refreshToken string) (resp *OAuthV2Response, err error) {
 	values := url.Values{
 		"client_id":     {clientID},
@@ -156,3 +166,24 @@ func RefreshOAuthV2TokenContext(ctx context.Context, client httpClient, clientID
 	}
 	return response, response.Err()
 }
+
+// GetOpenIDConnectToken exchanges a temporary OAuth verifier code for an access token for Sign in with Slack.
+// see: https://api.slack.com/methods/openid.connect.token
+func GetOpenIDConnectToken(client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OpenIDConnectResponse, err error) {
+	return GetOpenIDConnectTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI)
+}
+
+// GetOpenIDConnectTokenContext with a context, gets an access token for Sign in with Slack.
+func GetOpenIDConnectTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OpenIDConnectResponse, err error) {
+	values := url.Values{
+		"client_id":     {clientID},
+		"client_secret": {clientSecret},
+		"code":          {code},
+		"redirect_uri":  {redirectURI},
+	}
+	response := &OpenIDConnectResponse{}
+	if err = postForm(ctx, client, APIURL+"openid.connect.token", values, response, discard{}); err != nil {
+		return nil, err
+	}
+	return response, response.Err()
+}
diff --git a/reactions.go b/reactions.go
index 2a9bd42..39aa5ba 100644
--- a/reactions.go
+++ b/reactions.go
@@ -67,10 +67,11 @@ const (
 
 // ListReactionsParameters is the inputs to find all reactions by a user.
 type ListReactionsParameters struct {
-	User  string
-	Count int
-	Page  int
-	Full  bool
+	User   string
+	TeamID string
+	Count  int
+	Page   int
+	Full   bool
 }
 
 // NewListReactionsParameters initializes the inputs to find all reactions
@@ -246,6 +247,9 @@ func (api *Client) ListReactionsContext(ctx context.Context, params ListReaction
 	if params.User != DEFAULT_REACTIONS_USER {
 		values.Add("user", params.User)
 	}
+	if params.TeamID != "" {
+		values.Add("team_id", params.TeamID)
+	}
 	if params.Count != DEFAULT_REACTIONS_COUNT {
 		values.Add("count", strconv.Itoa(params.Count))
 	}
diff --git a/reminders.go b/reminders.go
index 53d67c0..5543a15 100644
--- a/reminders.go
+++ b/reminders.go
@@ -41,10 +41,7 @@ func (api *Client) doReminders(ctx context.Context, path string, values url.Valu
 
 	// create an array of pointers to reminders
 	var reminders = make([]*Reminder, 0, len(response.Reminders))
-	for _, reminder := range response.Reminders {
-		reminders = append(reminders, reminder)
-	}
-
+	reminders = append(reminders, response.Reminders...)
 	return reminders, response.Err()
 }
 
diff --git a/search.go b/search.go
index de6b40a..d27497a 100644
--- a/search.go
+++ b/search.go
@@ -15,6 +15,7 @@ const (
 )
 
 type SearchParameters struct {
+	TeamID        string
 	Sort          string
 	SortDirection string
 	Highlight     bool
@@ -93,6 +94,9 @@ func (api *Client) _search(ctx context.Context, path, query string, params Searc
 		"token": {api.token},
 		"query": {query},
 	}
+	if params.TeamID != "" {
+		values.Add("team_id", params.TeamID)
+	}
 	if params.Sort != DEFAULT_SEARCH_SORT {
 		values.Add("sort", params.Sort)
 	}
diff --git a/slack.go b/slack.go
index ea3aab6..756106f 100644
--- a/slack.go
+++ b/slack.go
@@ -57,12 +57,14 @@ type authTestResponseFull struct {
 type ParamOption func(*url.Values)
 
 type Client struct {
-	token         string
-	appLevelToken string
-	endpoint      string
-	debug         bool
-	log           ilogger
-	httpclient    httpClient
+	token              string
+	appLevelToken      string
+	configToken        string
+	configRefreshToken string
+	endpoint           string
+	debug              bool
+	log                ilogger
+	httpclient         httpClient
 }
 
 // Option defines an option for a Client
@@ -99,6 +101,16 @@ func OptionAppLevelToken(token string) func(*Client) {
 	return func(c *Client) { c.appLevelToken = token }
 }
 
+// OptionConfigToken sets a configuration token for the client.
+func OptionConfigToken(token string) func(*Client) {
+	return func(c *Client) { c.configToken = token }
+}
+
+// OptionConfigRefreshToken sets a configuration refresh token for the client.
+func OptionConfigRefreshToken(token string) func(*Client) {
+	return func(c *Client) { c.configRefreshToken = token }
+}
+
 // New builds a slack client from the provided token and options.
 func New(token string, options ...Option) *Client {
 	s := &Client{
diff --git a/slackevents/inner_events.go b/slackevents/inner_events.go
index fb1ab13..bc58426 100644
--- a/slackevents/inner_events.go
+++ b/slackevents/inner_events.go
@@ -28,6 +28,9 @@ type AppMentionEvent struct {
 
 	// BotID is filled out when a bot triggers the app_mention event
 	BotID string `json:"bot_id,omitempty"`
+
+	// When the app is mentioned in the edited message
+	Edited *Edited `json:"edited,omitempty"`
 }
 
 // AppHomeOpenedEvent Your Slack app home was opened.
@@ -256,6 +259,9 @@ type MessageEvent struct {
 	PreviousMessage *MessageEvent `json:"previous_message,omitempty"`
 	Edited          *Edited       `json:"edited,omitempty"`
 
+	// Deleted Message
+	DeletedTimeStamp string `json:"deleted_ts,omitempty"`
+
 	// Message Subtypes
 	SubType string `json:"subtype,omitempty"`
 
@@ -267,6 +273,7 @@ type MessageEvent struct {
 	Upload bool   `json:"upload"`
 	Files  []File `json:"files"`
 
+	Blocks      slack.Blocks       `json:"blocks,omitempty"`
 	Attachments []slack.Attachment `json:"attachments,omitempty"`
 
 	// Root is the message that was broadcast to the channel when the SubType is
@@ -544,6 +551,104 @@ type UserProfileChangedEvent struct {
 	EventTs string      `json:"event_ts"`
 }
 
+// SharedChannelInviteApprovedEvent is sent if your invitation has been approved
+type SharedChannelInviteApprovedEvent struct {
+	Type            string              `json:"type"`
+	Invite          *SharedInvite       `json:"invite"`
+	Channel         *slack.Conversation `json:"channel"`
+	ApprovingTeamID string              `json:"approving_team_id"`
+	TeamsInChannel  []*SlackEventTeam   `json:"teams_in_channel"`
+	ApprovingUser   *SlackEventUser     `json:"approving_user"`
+	EventTs         string              `json:"event_ts"`
+}
+
+// SharedChannelInviteAcceptedEvent is sent if external org accepts a Slack Connect channel invite
+type SharedChannelInviteAcceptedEvent struct {
+	Type                string            `json:"type"`
+	ApprovalRequired    bool              `json:"approval_required"`
+	Invite              *SharedInvite     `json:"invite"`
+	Channel             *SharedChannel    `json:"channel"`
+	TeamsInChannel      []*SlackEventTeam `json:"teams_in_channel"`
+	AcceptingUser       *SlackEventUser   `json:"accepting_user"`
+	EventTs             string            `json:"event_ts"`
+	RequiresSponsorship bool              `json:"requires_sponsorship,omitempty"`
+}
+
+// SharedChannelInviteDeclinedEvent is sent if external or internal org declines the Slack Connect invite
+type SharedChannelInviteDeclinedEvent struct {
+	Type            string            `json:"type"`
+	Invite          *SharedInvite     `json:"invite"`
+	Channel         *SharedChannel    `json:"channel"`
+	DecliningTeamID string            `json:"declining_team_id"`
+	TeamsInChannel  []*SlackEventTeam `json:"teams_in_channel"`
+	DecliningUser   *SlackEventUser   `json:"declining_user"`
+	EventTs         string            `json:"event_ts"`
+}
+
+// SharedChannelInviteReceivedEvent is sent if a bot or app is invited to a Slack Connect channel
+type SharedChannelInviteReceivedEvent struct {
+	Type    string         `json:"type"`
+	Invite  *SharedInvite  `json:"invite"`
+	Channel *SharedChannel `json:"channel"`
+	EventTs string         `json:"event_ts"`
+}
+
+// SlackEventTeam is a struct for teams in ShareChannel events
+type SlackEventTeam struct {
+	ID                  string          `json:"id"`
+	Name                string          `json:"name"`
+	Icon                *SlackEventIcon `json:"icon,omitempty"`
+	AvatarBaseURL       string          `json:"avatar_base_url,omitempty"`
+	IsVerified          bool            `json:"is_verified"`
+	Domain              string          `json:"domain"`
+	DateCreated         int             `json:"date_created"`
+	RequiresSponsorship bool            `json:"requires_sponsorship,omitempty"`
+	// TeamID              string          `json:"team_id,omitempty"`
+}
+
+// SlackEventIcon is a struct for icons in ShareChannel events
+type SlackEventIcon struct {
+	ImageDefault bool   `json:"image_default,omitempty"`
+	Image34      string `json:"image_34,omitempty"`
+	Image44      string `json:"image_44,omitempty"`
+	Image68      string `json:"image_68,omitempty"`
+	Image88      string `json:"image_88,omitempty"`
+	Image102     string `json:"image_102,omitempty"`
+	Image132     string `json:"image_132,omitempty"`
+	Image230     string `json:"image_230,omitempty"`
+}
+
+// SlackEventUser is a struct for users in ShareChannel events
+type SlackEventUser struct {
+	ID                     string             `json:"id"`
+	TeamID                 string             `json:"team_id"`
+	Name                   string             `json:"name"`
+	Updated                int                `json:"updated,omitempty"`
+	Profile                *slack.UserProfile `json:"profile,omitempty"`
+	WhoCanShareContactCard string             `json:"who_can_share_contact_card,omitempty"`
+}
+
+// SharedChannel is a struct for shared channels in ShareChannel events
+type SharedChannel struct {
+	ID        string `json:"id"`
+	IsPrivate bool   `json:"is_private"`
+	IsIm      bool   `json:"is_im"`
+	Name      string `json:"name,omitempty"`
+}
+
+// SharedInvite is a struct for shared invites in ShareChannel events
+type SharedInvite struct {
+	ID                string          `json:"id"`
+	DateCreated       int             `json:"date_created"`
+	DateInvalid       int             `json:"date_invalid"`
+	InvitingTeam      *SlackEventTeam `json:"inviting_team,omitempty"`
+	InvitingUser      *SlackEventUser `json:"inviting_user,omitempty"`
+	RecipientEmail    string          `json:"recipient_email,omitempty"`
+	RecipientUserID   string          `json:"recipient_user_id,omitempty"`
+	IsSponsored       bool            `json:"is_sponsored,omitempty"`
+	IsExternalLimited bool            `json:"is_external_limited,omitempty"`
+}
+
 type EventsAPIType string
 
 const (
@@ -607,6 +712,14 @@ const (
 	ReactionRemoved = EventsAPIType("reaction_removed")
 	// TeamJoin A new user joined the workspace
 	TeamJoin = EventsAPIType("team_join")
+	// Slack connect app or bot invite received
+	SharedChannelInviteReceived = EventsAPIType("shared_channel_invite_received")
+	// Slack connect channel invite approved
+	SharedChannelInviteApproved = EventsAPIType("shared_channel_invite_approved")
+	// Slack connect channel invite declined
+	SharedChannelInviteDeclined = EventsAPIType("shared_channel_invite_declined")
+	// Slack connect channel invite accepted by an end user
+	SharedChannelInviteAccepted = EventsAPIType("shared_channel_invite_accepted")
 	// TokensRevoked APP's API tokes are revoked
 	TokensRevoked = EventsAPIType("tokens_revoked")
 	// EmojiChanged A custom emoji has been added or changed
@@ -631,43 +744,47 @@ const (
 // implementations. The structs should be instances of the unmarshalling
 // target for the matching event type.
 var EventsAPIInnerEventMapping = map[EventsAPIType]interface{}{
-	AppMention:             AppMentionEvent{},
-	AppHomeOpened:          AppHomeOpenedEvent{},
-	AppUninstalled:         AppUninstalledEvent{},
-	ChannelCreated:         ChannelCreatedEvent{},
-	ChannelDeleted:         ChannelDeletedEvent{},
-	ChannelArchive:         ChannelArchiveEvent{},
-	ChannelUnarchive:       ChannelUnarchiveEvent{},
-	ChannelLeft:            ChannelLeftEvent{},
-	ChannelRename:          ChannelRenameEvent{},
-	ChannelIDChanged:       ChannelIDChangedEvent{},
-	FileChange:             FileChangeEvent{},
-	FileDeleted:            FileDeletedEvent{},
-	FileShared:             FileSharedEvent{},
-	FileUnshared:           FileUnsharedEvent{},
-	GroupDeleted:           GroupDeletedEvent{},
-	GroupArchive:           GroupArchiveEvent{},
-	GroupUnarchive:         GroupUnarchiveEvent{},
-	GroupLeft:              GroupLeftEvent{},
-	GroupRename:            GroupRenameEvent{},
-	GridMigrationFinished:  GridMigrationFinishedEvent{},
-	GridMigrationStarted:   GridMigrationStartedEvent{},
-	LinkShared:             LinkSharedEvent{},
-	Message:                MessageEvent{},
-	MemberJoinedChannel:    MemberJoinedChannelEvent{},
-	MemberLeftChannel:      MemberLeftChannelEvent{},
-	PinAdded:               PinAddedEvent{},
-	PinRemoved:             PinRemovedEvent{},
-	ReactionAdded:          ReactionAddedEvent{},
-	ReactionRemoved:        ReactionRemovedEvent{},
-	TeamJoin:               TeamJoinEvent{},
-	TokensRevoked:          TokensRevokedEvent{},
-	EmojiChanged:           EmojiChangedEvent{},
-	WorkflowStepExecute:    WorkflowStepExecuteEvent{},
-	MessageMetadataPosted:  MessageMetadataPostedEvent{},
-	MessageMetadataUpdated: MessageMetadataUpdatedEvent{},
-	MessageMetadataDeleted: MessageMetadataDeletedEvent{},
-	TeamAccessGranted:      TeamAccessGrantedEvent{},
-	TeamAccessRevoked:      TeamAccessRevokedEvent{},
-	UserProfileChanged:     UserProfileChangedEvent{},
+	AppMention:                  AppMentionEvent{},
+	AppHomeOpened:               AppHomeOpenedEvent{},
+	AppUninstalled:              AppUninstalledEvent{},
+	ChannelCreated:              ChannelCreatedEvent{},
+	ChannelDeleted:              ChannelDeletedEvent{},
+	ChannelArchive:              ChannelArchiveEvent{},
+	ChannelUnarchive:            ChannelUnarchiveEvent{},
+	ChannelLeft:                 ChannelLeftEvent{},
+	ChannelRename:               ChannelRenameEvent{},
+	ChannelIDChanged:            ChannelIDChangedEvent{},
+	FileChange:                  FileChangeEvent{},
+	FileDeleted:                 FileDeletedEvent{},
+	FileShared:                  FileSharedEvent{},
+	FileUnshared:                FileUnsharedEvent{},
+	GroupDeleted:                GroupDeletedEvent{},
+	GroupArchive:                GroupArchiveEvent{},
+	GroupUnarchive:              GroupUnarchiveEvent{},
+	GroupLeft:                   GroupLeftEvent{},
+	GroupRename:                 GroupRenameEvent{},
+	GridMigrationFinished:       GridMigrationFinishedEvent{},
+	GridMigrationStarted:        GridMigrationStartedEvent{},
+	LinkShared:                  LinkSharedEvent{},
+	Message:                     MessageEvent{},
+	MemberJoinedChannel:         MemberJoinedChannelEvent{},
+	MemberLeftChannel:           MemberLeftChannelEvent{},
+	PinAdded:                    PinAddedEvent{},
+	PinRemoved:                  PinRemovedEvent{},
+	ReactionAdded:               ReactionAddedEvent{},
+	ReactionRemoved:             ReactionRemovedEvent{},
+	SharedChannelInviteApproved: SharedChannelInviteApprovedEvent{},
+	SharedChannelInviteAccepted: SharedChannelInviteAcceptedEvent{},
+	SharedChannelInviteDeclined: SharedChannelInviteDeclinedEvent{},
+	SharedChannelInviteReceived: SharedChannelInviteReceivedEvent{},
+	TeamJoin:                    TeamJoinEvent{},
+	TokensRevoked:               TokensRevokedEvent{},
+	EmojiChanged:                EmojiChangedEvent{},
+	WorkflowStepExecute:         WorkflowStepExecuteEvent{},
+	MessageMetadataPosted:       MessageMetadataPostedEvent{},
+	MessageMetadataUpdated:      MessageMetadataUpdatedEvent{},
+	MessageMetadataDeleted:      MessageMetadataDeletedEvent{},
+	TeamAccessGranted:           TeamAccessGrantedEvent{},
+	TeamAccessRevoked:           TeamAccessRevokedEvent{},
+	UserProfileChanged:          UserProfileChangedEvent{},
 }
diff --git a/slackevents/parsers.go b/slackevents/parsers.go
index 96ba568..4daac8b 100644
--- a/slackevents/parsers.go
+++ b/slackevents/parsers.go
@@ -117,7 +117,7 @@ func parseInnerEvent(e *EventsAPICallbackEvent) (EventsAPIEvent, error) {
 			e.EnterpriseID,
 			nil,
 			EventsAPIInnerEvent{},
-		}, fmt.Errorf("Inner Event does not exist! %s", iE.Type)
+		}, fmt.Errorf("inner Event does not exist! %s", iE.Type)
 	}
 	t := reflect.TypeOf(v)
 	recvEvent := reflect.New(t).Interface()
@@ -192,7 +192,7 @@ func ParseEvent(rawEvent json.RawMessage, opts ...Option) (EventsAPIEvent, error
 	}
 
 	if !cfg.TokenVerified {
-		return EventsAPIEvent{}, errors.New("Invalid verification token")
+		return EventsAPIEvent{}, errors.New("invalid verification token")
 	}
 
 	if e.Type == CallbackEvent {
diff --git a/slash.go b/slash.go
index b2c5094..fd46abf 100644
--- a/slash.go
+++ b/slash.go
@@ -1,25 +1,29 @@
 package slack
 
 import (
+	"encoding/json"
+	"fmt"
 	"net/http"
+	"strconv"
 )
 
 // SlashCommand contains information about a request of the slash command
 type SlashCommand struct {
-	Token          string `json:"token"`
-	TeamID         string `json:"team_id"`
-	TeamDomain     string `json:"team_domain"`
-	EnterpriseID   string `json:"enterprise_id,omitempty"`
-	EnterpriseName string `json:"enterprise_name,omitempty"`
-	ChannelID      string `json:"channel_id"`
-	ChannelName    string `json:"channel_name"`
-	UserID         string `json:"user_id"`
-	UserName       string `json:"user_name"`
-	Command        string `json:"command"`
-	Text           string `json:"text"`
-	ResponseURL    string `json:"response_url"`
-	TriggerID      string `json:"trigger_id"`
-	APIAppID       string `json:"api_app_id"`
+	Token               string `json:"token"`
+	TeamID              string `json:"team_id"`
+	TeamDomain          string `json:"team_domain"`
+	EnterpriseID        string `json:"enterprise_id,omitempty"`
+	EnterpriseName      string `json:"enterprise_name,omitempty"`
+	IsEnterpriseInstall bool   `json:"is_enterprise_install"`
+	ChannelID           string `json:"channel_id"`
+	ChannelName         string `json:"channel_name"`
+	UserID              string `json:"user_id"`
+	UserName            string `json:"user_name"`
+	Command             string `json:"command"`
+	Text                string `json:"text"`
+	ResponseURL         string `json:"response_url"`
+	TriggerID           string `json:"trigger_id"`
+	APIAppID            string `json:"api_app_id"`
 }
 
 // SlashCommandParse will parse the request of the slash command
@@ -32,6 +36,7 @@ func SlashCommandParse(r *http.Request) (s SlashCommand, err error) {
 	s.TeamDomain = r.PostForm.Get("team_domain")
 	s.EnterpriseID = r.PostForm.Get("enterprise_id")
 	s.EnterpriseName = r.PostForm.Get("enterprise_name")
+	s.IsEnterpriseInstall = r.PostForm.Get("is_enterprise_install") == "true"
 	s.ChannelID = r.PostForm.Get("channel_id")
 	s.ChannelName = r.PostForm.Get("channel_name")
 	s.UserID = r.PostForm.Get("user_id")
@@ -53,3 +58,34 @@ func (s SlashCommand) ValidateToken(verificationTokens ...string) bool {
 	}
 	return false
 }
+
+// UnmarshalJSON handles is_enterprise_install being either a boolean or a
+// string when parsing JSON from various payloads
+func (s *SlashCommand) UnmarshalJSON(data []byte) error {
+	type SlashCommandCopy SlashCommand
+	scopy := &struct {
+		*SlashCommandCopy
+		IsEnterpriseInstall interface{} `json:"is_enterprise_install"`
+	}{
+		SlashCommandCopy: (*SlashCommandCopy)(s),
+	}
+
+	if err := json.Unmarshal(data, scopy); err != nil {
+		return err
+	}
+
+	switch rawValue := scopy.IsEnterpriseInstall.(type) {
+	case string:
+		b, err := strconv.ParseBool(rawValue)
+		if err != nil {
+			return fmt.Errorf("parsing boolean for is_enterprise_install: %w", err)
+		}
+		s.IsEnterpriseInstall = b
+	case bool:
+		s.IsEnterpriseInstall = rawValue
+	default:
+		return fmt.Errorf("wrong data type for is_enterprise_install: %T", scopy.IsEnterpriseInstall)
+	}
+
+	return nil
+}
diff --git a/socketmode/deadman.go b/socketmode/deadman.go
deleted file mode 100644
index 7aeea76..0000000
--- a/socketmode/deadman.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package socketmode
-
-import "time"
-
-type deadmanTimer struct {
-	timeout time.Duration
-	timer   *time.Timer
-}
-
-func newDeadmanTimer(timeout time.Duration) *deadmanTimer {
-	return &deadmanTimer{
-		timeout: timeout,
-		timer:   time.NewTimer(timeout),
-	}
-}
-
-func (smc *deadmanTimer) Elapsed() <-chan time.Time {
-	return smc.timer.C
-}
-
-func (smc *deadmanTimer) Reset() {
-	// Note that this is the correct way to Reset a non-expired timer
-	if !smc.timer.Stop() {
-		select {
-		case <-smc.timer.C:
-		default:
-		}
-	}
-
-	smc.timer.Reset(smc.timeout)
-}
diff --git a/socketmode/socket_mode_managed_conn.go b/socketmode/socket_mode_managed_conn.go
index 5a98f1e..b94456f 100644
--- a/socketmode/socket_mode_managed_conn.go
+++ b/socketmode/socket_mode_managed_conn.go
@@ -1,6 +1,7 @@
 package socketmode
 
 import (
+	"bytes"
 	"context"
 	"encoding/json"
 	"errors"
@@ -53,13 +54,14 @@ func (smc *Client) RunContext(ctx context.Context) error {
 }
 
 func (smc *Client) run(ctx context.Context, connectionCount int) error {
-	messages := make(chan json.RawMessage)
-	defer close(messages)
-
-	deadmanTimer := newDeadmanTimer(smc.maxPingInterval)
+	messages := make(chan json.RawMessage, 1)
 
+	pingChan := make(chan time.Time, 1)
 	pingHandler := func(_ string) error {
-		deadmanTimer.Reset()
+		select {
+		case pingChan <- time.Now():
+		default:
+		}
 
 		return nil
 	}
@@ -81,20 +83,24 @@ func (smc *Client) run(ctx context.Context, connectionCount int) error {
 	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
 
-	smc.Events <- newEvent(EventTypeConnected, &ConnectedEvent{
+	smc.sendEvent(ctx, newEvent(EventTypeConnected, &ConnectedEvent{
 		ConnectionCount: connectionCount,
 		Info:            info,
-	})
+	}))
 
 	smc.Debugf("WebSocket connection succeeded on try %d", connectionCount)
 
 	// We're now connected so we can set up listeners
 
-	var (
-		wg           sync.WaitGroup
-		firstErr     error
-		firstErrOnce sync.Once
-	)
+	wg := new(sync.WaitGroup)
+	// sendErr relies on the buffer of 1 here
+	errc := make(chan error, 1)
+	sendErr := func(err error) {
+		select {
+		case errc <- err:
+		default:
+		}
+	}
 
 	wg.Add(1)
 	go func() {
@@ -103,9 +109,7 @@ func (smc *Client) run(ctx context.Context, connectionCount int) error {
 
 		// The response sender sends Socket Mode responses over the WebSocket conn
 		if err := smc.runResponseSender(ctx, conn); err != nil {
-			firstErrOnce.Do(func() {
-				firstErr = err
-			})
+			sendErr(err)
 		}
 	}()
 
@@ -116,56 +120,79 @@ func (smc *Client) run(ctx context.Context, connectionCount int) error {
 
 		// The handler reads Socket Mode requests, and enqueues responses for sending by the response sender
 		if err := smc.runRequestHandler(ctx, messages); err != nil {
-			firstErrOnce.Do(func() {
-				firstErr = err
-			})
+			sendErr(err)
 		}
 	}()
 
-	// Need to wait for runMessageReceiver to avoid panic described in https://github.com/slack-go/slack/issues/1125
-	wg.Add(1)
 	go func() {
-		defer wg.Done()
 		defer cancel()
+		// We close messages here as it is the producer for the channel.
+		defer close(messages)
 
 		// The receiver reads WebSocket messages, and enqueues parsed Socket Mode requests to be handled by
 		// the request handler
 		if err := smc.runMessageReceiver(ctx, conn, messages); err != nil {
-			firstErrOnce.Do(func() {
-				firstErr = err
-			})
+			sendErr(err)
 		}
 	}()
 
 	wg.Add(1)
-	go func() {
+	go func(pingInterval time.Duration) {
 		defer wg.Done()
-
-		select {
-		case <-ctx.Done():
+		defer func() {
 			// Detect when the connection is dead and try close connection.
-			if err = conn.Close(); err != nil {
+			if err := conn.Close(); err != nil {
 				smc.Debugf("Failed to close connection: %v", err)
 			}
-		case <-deadmanTimer.Elapsed():
-			firstErrOnce.Do(func() {
-				firstErr = errors.New("ping timeout: Slack did not send us WebSocket PING for more than Client.maxInterval")
-			})
+		}()
+
+		done := ctx.Done()
+		var lastPing time.Time
+
+		// More efficient than constantly resetting a timer w/ Stop+Reset
+		ticker := time.NewTicker(pingInterval)
+		defer ticker.Stop()
 
-			cancel()
+		for {
+			select {
+			case <-done:
+				return
+
+			case lastPing = <-pingChan:
+				// This case gets the time of the last ping.
+				// If this case never fires then the pingHandler was never called
+				// in which case lastPing is the zero time.Time value, and will 'fail'
+				// the next tick, causing us to exit.
+
+			case now := <-ticker.C:
+				// Our last ping is older than our interval
+				if now.Sub(lastPing) > pingInterval {
+					sendErr(errors.New("ping timeout: Slack did not send us WebSocket PING for more than Client.maxInterval"))
+
+					cancel()
+					return
+				}
+			}
 		}
-	}()
+	}(smc.maxPingInterval)
 
 	wg.Wait()
 
-	if firstErr == context.Canceled {
-		return firstErr
+	select {
+	case err = <-errc:
+		// Get buffered error
+	default:
+		// Or nothing if they all exited nil
+	}
+
+	if errors.Is(err, context.Canceled) {
+		return err
 	}
 
 	// wg.Wait() finishes only after any of the above go routines finishes and cancels the
 	// context, allowing the other threads to shut down gracefully.
-	// Also, we can expect firstErr to be not nil, as goroutines can finish only on error.
-	smc.Debugf("Reconnecting due to %v", firstErr)
+	// Also, we can expect our (first)err to be not nil, as goroutines can finish only on error.
+	smc.Debugf("Reconnecting due to %v", err)
 
 	return nil
 }
@@ -193,10 +220,10 @@ func (smc *Client) connect(ctx context.Context, connectionCount int, additionalP
 		)
 
 		// send connecting event
-		smc.Events <- newEvent(EventTypeConnecting, &slack.ConnectingEvent{
+		smc.sendEvent(ctx, newEvent(EventTypeConnecting, &slack.ConnectingEvent{
 			Attempt:         boff.Attempts() + 1,
 			ConnectionCount: connectionCount,
-		})
+		}))
 
 		// attempt to start the connection
 		info, conn, err := smc.openAndDial(ctx, additionalPingHandler)
@@ -212,26 +239,32 @@ func (smc *Client) connect(ctx context.Context, connectionCount int, additionalP
 		default:
 		}
 
-		switch actual := err.(type) {
-		case slack.StatusCodeError:
-			if actual.Code == http.StatusNotFound {
-				smc.Debugf("invalid auth when connecting with Socket Mode: %s", err)
-				smc.Events <- newEvent(EventTypeInvalidAuth, &slack.InvalidAuthEvent{})
-				return nil, nil, err
-			}
-		case *slack.RateLimitedError:
-			backoff = actual.RetryAfter
-		default:
+		var (
+			actual  slack.StatusCodeError
+			rlError *slack.RateLimitedError
+		)
+
+		if errors.As(err, &actual) && actual.Code == http.StatusNotFound {
+			smc.Debugf("invalid auth when connecting with Socket Mode: %s", err)
+			smc.sendEvent(ctx, newEvent(EventTypeInvalidAuth, &slack.InvalidAuthEvent{}))
+
+			return nil, nil, err
+		} else if errors.As(err, &rlError) {
+			backoff = rlError.RetryAfter
 		}
 
+		// If we check for errors.Is(err, context.Canceled) here and
+		// return early then we don't send the Event below that some users
+		// may already rely on; ie a behavior change.
+
 		backoff = timex.Max(backoff, boff.Duration())
 		// any other errors are treated as recoverable and we try again after
 		// sending the event along the Events channel
-		smc.Events <- newEvent(EventTypeConnectionError, &slack.ConnectionErrorEvent{
+		smc.sendEvent(ctx, newEvent(EventTypeConnectionError, &slack.ConnectionErrorEvent{
 			Attempt:  boff.Attempts(),
 			Backoff:  backoff,
 			ErrorObj: err,
-		})
+		}))
 
 		// get time we should wait before attempting to connect again
 		smc.Debugf("reconnection %d failed: %s reconnecting in %v\n", boff.Attempts(), err, backoff)
@@ -239,9 +272,11 @@ func (smc *Client) connect(ctx context.Context, connectionCount int, additionalP
 		// wait for one of the following to occur,
 		// backoff duration has elapsed, disconnectCh is signalled, or
 		// the smc finishes disconnecting.
+		timer := time.NewTimer(backoff)
 		select {
-		case <-time.After(backoff): // retry after the backoff.
+		case <-timer.C: // retry after the backoff.
 		case <-ctx.Done():
+			timer.Stop()
 			return nil, nil, ctx.Err()
 		}
 	}
@@ -276,12 +311,13 @@ func (smc *Client) openAndDial(ctx context.Context, additionalPingHandler func(s
 		smc.Debugf("Failed to dial to the websocket: %s", err)
 		return nil, nil, err
 	}
+	if additionalPingHandler == nil {
+		additionalPingHandler = func(_ string) error { return nil }
+	}
 
 	conn.SetPingHandler(func(appData string) error {
-		if additionalPingHandler != nil {
-			if err := additionalPingHandler(appData); err != nil {
-				return err
-			}
+		if err := additionalPingHandler(appData); err != nil {
+			return err
 		}
 
 		smc.handlePing(conn, appData)
@@ -312,10 +348,10 @@ func (smc *Client) runResponseSender(ctx context.Context, conn *websocket.Conn)
 			smc.Debugf("Sending Socket Mode response with envelope ID %q: %v", res.EnvelopeID, res)
 
 			if err := unsafeWriteSocketModeResponse(conn, res); err != nil {
-				smc.Events <- newEvent(EventTypeErrorWriteFailed, &ErrorWriteFailed{
+				smc.sendEvent(ctx, newEvent(EventTypeErrorWriteFailed, &ErrorWriteFailed{
 					Cause:    err,
 					Response: res,
-				})
+				}))
 			}
 
 			smc.Debugf("Finished sending Socket Mode response with envelope ID %q", res.EnvelopeID)
@@ -332,16 +368,22 @@ func (smc *Client) runRequestHandler(ctx context.Context, websocket chan json.Ra
 		select {
 		case <-ctx.Done():
 			return ctx.Err()
-		case message := <-websocket:
+		case message, ok := <-websocket:
+			if !ok {
+				// The producer closed the channel because it encountered an error (or panic),
+				// we need only return.
+				return nil
+			}
+
 			smc.Debugf("Received WebSocket message: %s", message)
 
 			// listen for incoming messages that need to be parsed
 			evt, err := smc.parseEvent(message)
 			if err != nil {
-				smc.Events <- newEvent(EventTypeErrorBadMessage, &ErrorBadMessage{
+				smc.sendEvent(ctx, newEvent(EventTypeErrorBadMessage, &ErrorBadMessage{
 					Cause:   err,
 					Message: message,
-				})
+				}))
 			} else if evt != nil {
 				if evt.Type == EventTypeDisconnect {
 					// We treat the `disconnect` request from Slack as an error internally,
@@ -349,7 +391,7 @@ func (smc *Client) runRequestHandler(ctx context.Context, websocket chan json.Ra
 					return errorRequestedDisconnect{}
 				}
 
-				smc.Events <- *evt
+				smc.sendEvent(ctx, *evt)
 			}
 		}
 	}
@@ -385,11 +427,7 @@ func unsafeWriteSocketModeResponse(conn *websocket.Conn, res *Response) error {
 	// Remove write deadline regardless of WriteJSON succeeds or not
 	defer conn.SetWriteDeadline(time.Time{})
 
-	if err := conn.WriteJSON(res); err != nil {
-		return err
-	}
-
-	return nil
+	return conn.WriteJSON(res)
 }
 
 func newEvent(tpe EventType, data interface{}, req ...*Request) Event {
@@ -407,29 +445,54 @@ func newEvent(tpe EventType, data interface{}, req ...*Request) Event {
 // This tells Slack that the we have received the request denoted by the envelope ID,
 // by sending back the envelope ID over the WebSocket connection.
 func (smc *Client) Ack(req Request, payload ...interface{}) {
-	res := Response{
-		EnvelopeID: req.EnvelopeID,
-	}
-
+	var pld interface{}
 	if len(payload) > 0 {
-		res.Payload = payload[0]
+		pld = payload[0]
 	}
 
-	smc.Send(res)
+	smc.AckCtx(context.TODO(), req.EnvelopeID, pld)
+}
+
+// AckCtx acknowledges the Socket Mode request envelope ID with the payload.
+//
+// This tells Slack that the we have received the request denoted by the request (envelope) ID,
+// by sending back the ID over the WebSocket connection.
+func (smc *Client) AckCtx(ctx context.Context, reqID string, payload interface{}) error {
+	return smc.SendCtx(ctx, Response{
+		EnvelopeID: reqID,
+		Payload:    payload,
+	})
 }
 
 // Send sends the Socket Mode response over a WebSocket connection.
 // This is usually used for acknowledging requests, but if you need more control over Client.Ack().
 // It's normally recommended to use Client.Ack() instead of this.
 func (smc *Client) Send(res Response) {
-	js, err := json.Marshal(res)
-	if err != nil {
-		panic(err)
+	smc.SendCtx(context.TODO(), res)
+}
+
+// SendCtx sends the Socket Mode response over a WebSocket connection.
+// This is usually used for acknowledging requests, but if you need more control
+// it's normally recommended to use Client.AckCtx() instead of this.
+func (smc *Client) SendCtx(ctx context.Context, res Response) error {
+	if smc.debug {
+		js, err := json.Marshal(res)
+
+		// Log the error so users of `Send` don't see it entirely disappear as that method
+		// does not return an error and used to panic on failure (with or without debug)
+		smc.Debugf("Scheduling Socket Mode response (error: %v) for envelope ID %s: %s", err, res.EnvelopeID, js)
+		if err != nil {
+			return err
+		}
 	}
 
-	smc.Debugf("Scheduling Socket Mode response for envelope ID %s: %s", res.EnvelopeID, js)
+	select {
+	case <-ctx.Done():
+		return ctx.Err()
+	case smc.socketModeResponses <- &res:
+	}
 
-	smc.socketModeResponses <- &res
+	return nil
 }
 
 // receiveMessagesInto attempts to receive an event from the WebSocket connection for Socket Mode.
@@ -439,75 +502,58 @@ func (smc *Client) receiveMessagesInto(ctx context.Context, conn *websocket.Conn
 	smc.Debugf("Starting to receive message")
 	defer smc.Debugf("Finished to receive message")
 
-	var err error
-
 	event := json.RawMessage{}
-
-	readJsonErr := make(chan error)
-	go func() {
-		select {
-		case readJsonErr <- conn.ReadJSON(&event):
-			// if conn.ReadJSON method returns after ctx.Done(), no one will read the unbuffered channel
-			// so, to avoid goroutines leak we need to check for ctx.Done() here too.
-			// need to say here, that conn.ReadJSON will really return after ctx.Done(), because in that case
-			// conn is closed
-			break
-		case <-ctx.Done():
-			// just need to listen ctx.Done, nothing has to be done here
-			break
-		}
-	}()
-
-	select {
-	case err = <-readJsonErr:
-		// we have awaited response from conn.ReadJSON, so, we can handle error now
-		break
-	case <-ctx.Done():
-		// context cancellation signal got, closing connection and returning
-		cerr := conn.Close()
-		if cerr != nil {
-			smc.Debugf("Failed to close connection: %v", cerr)
+	err := conn.ReadJSON(&event)
+	if err != nil {
+		// check if the connection was closed.
+		// This version of the gorilla/websocket package also does a type assertion
+		// on the error, rather than unwrapping it, so we'll do the unwrapping then pass
+		// the unwrapped error
+		var wsErr *websocket.CloseError
+		if errors.As(err, &wsErr) && websocket.IsUnexpectedCloseError(wsErr) {
+			return err
 		}
 
-		return ctx.Err()
-	}
-
-	// after select above, the err will be set to result from conn.ReadJSON, handling
-
-	// check if the connection was closed.
-	if websocket.IsUnexpectedCloseError(err) {
-		return err
-	}
+		if errors.Is(err, io.ErrUnexpectedEOF) {
+			// EOF's don't seem to signify a failed connection so instead we ignore
+			// them here and detect a failed connection upon attempting to send a
+			// 'PING' message
 
-	switch {
-	case err == io.ErrUnexpectedEOF:
-		// EOF's don't seem to signify a failed connection so instead we ignore
-		// them here and detect a failed connection upon attempting to send a
-		// 'PING' message
+			// Unlike RTM, we don't ping from the our end as there seem to have no client ping.
+			// We just continue to the next loop so that we `smc.disconnected` should be received if
+			// this EOF error was actually due to disconnection.
 
-		// Unlike RTM, we don't ping from the our end as there seem to have no client ping.
-		// We just continue to the next loop so that we `smc.disconnected` should be received if
-		// this EOF error was actually due to disconnection.
+			return nil
+		}
 
-		return nil
-	case err != nil:
 		// All other errors from ReadJSON come from NextReader, and should
 		// kill the read loop and force a reconnect.
-		smc.Events <- newEvent(EventTypeIncomingError, &slack.IncomingEventError{
+		// TODO: Unless it's a JSON unmarshal-type error in which case maybe reconnecting isn't needed...
+		smc.sendEvent(ctx, newEvent(EventTypeIncomingError, &slack.IncomingEventError{
 			ErrorObj: err,
-		})
+		}))
 
 		return err
-	case len(event) == 0:
-		smc.Debugln("Received empty event")
-	default:
-		select {
-		case sink <- event:
-		case <-ctx.Done():
-			smc.Debugln("cancelled while attempting to send raw event")
+	}
 
-			return ctx.Err()
+	if smc.debug {
+		buf := &bytes.Buffer{}
+		d := json.NewEncoder(buf)
+		d.SetIndent("", "  ")
+		if err := d.Encode(event); err != nil {
+			smc.Debugln("Failed encoding decoded json:", err)
 		}
+		reencoded := buf.String()
+
+		smc.Debugln("Incoming WebSocket message:", reencoded)
+	}
+
+	select {
+	case sink <- event:
+	case <-ctx.Done():
+		smc.Debugln("cancelled while attempting to send raw event")
+
+		return ctx.Err()
 	}
 
 	return nil
@@ -520,7 +566,7 @@ func (smc *Client) parseEvent(wsMsg json.RawMessage) (*Event, error) {
 	req := &Request{}
 	err := json.Unmarshal(wsMsg, req)
 	if err != nil {
-		return nil, fmt.Errorf("unmarshalling WebSocket message: %v", err)
+		return nil, fmt.Errorf("unmarshalling WebSocket message: %w", err)
 	}
 
 	var evt Event
@@ -536,7 +582,7 @@ func (smc *Client) parseEvent(wsMsg json.RawMessage) (*Event, error) {
 
 		eventsAPIEvent, err := slackevents.ParseEvent(payloadEvent, slackevents.OptionNoVerifyToken())
 		if err != nil {
-			return nil, fmt.Errorf("parsing Events API event: %v", err)
+			return nil, fmt.Errorf("parsing Events API event: %w", err)
 		}
 
 		evt = newEvent(EventTypeEventsAPI, eventsAPIEvent, req)
@@ -549,7 +595,7 @@ func (smc *Client) parseEvent(wsMsg json.RawMessage) (*Event, error) {
 		var cmd slack.SlashCommand
 
 		if err := json.Unmarshal(req.Payload, &cmd); err != nil {
-			return nil, fmt.Errorf("parsing slash command: %v", err)
+			return nil, fmt.Errorf("parsing slash command: %w", err)
 		}
 
 		evt = newEvent(EventTypeSlashCommand, cmd, req)
@@ -563,7 +609,7 @@ func (smc *Client) parseEvent(wsMsg json.RawMessage) (*Event, error) {
 		var callback slack.InteractionCallback
 
 		if err := json.Unmarshal(req.Payload, &callback); err != nil {
-			return nil, fmt.Errorf("parsing interaction callback: %v", err)
+			return nil, fmt.Errorf("parsing interaction callback: %w", err)
 		}
 
 		evt = newEvent(EventTypeInteractive, callback, req)
diff --git a/socketmode/socketmode.go b/socketmode/socketmode.go
index 1871e67..6ca8f48 100644
--- a/socketmode/socketmode.go
+++ b/socketmode/socketmode.go
@@ -119,3 +119,15 @@ func New(api *slack.Client, options ...Option) *Client {
 
 	return result
 }
+
+// sendEvent safely sends an event into the Clients Events channel
+// and blocks until buffer space is had, or the context is canceled.
+// This prevents deadlocking in the event that Events buffer is full,
+// other goroutines are waiting, and/or timing allows receivers to exit
+// before all senders are finished.
+func (smc *Client) sendEvent(ctx context.Context, event Event) {
+	select {
+	case smc.Events <- event:
+	case <-ctx.Done():
+	}
+}
diff --git a/team.go b/team.go
index d21a1b6..40a0f00 100644
--- a/team.go
+++ b/team.go
@@ -74,8 +74,9 @@ type BillingActive struct {
 
 // AccessLogParameters contains all the parameters necessary (including the optional ones) for a GetAccessLogs() request
 type AccessLogParameters struct {
-	Count int
-	Page  int
+	TeamID string
+	Count  int
+	Page   int
 }
 
 // NewAccessLogParameters provides an instance of AccessLogParameters with all the sane default values set
@@ -164,22 +165,24 @@ func (api *Client) GetTeamInfoContext(ctx context.Context) (*TeamInfo, error) {
 }
 
 // GetTeamProfile gets the Team Profile settings of the user
-func (api *Client) GetTeamProfile() (*TeamProfile, error) {
-	return api.GetTeamProfileContext(context.Background())
+func (api *Client) GetTeamProfile(teamID ...string) (*TeamProfile, error) {
+	return api.GetTeamProfileContext(context.Background(), teamID...)
 }
 
 // GetTeamProfileContext gets the Team Profile settings of the user with a custom context
-func (api *Client) GetTeamProfileContext(ctx context.Context) (*TeamProfile, error) {
+func (api *Client) GetTeamProfileContext(ctx context.Context, teamID ...string) (*TeamProfile, error) {
 	values := url.Values{
 		"token": {api.token},
 	}
+	if len(teamID) > 0 {
+		values["team_id"] = teamID
+	}
 
 	response, err := api.teamProfileRequest(ctx, api.httpclient, "team.profile.get", values)
 	if err != nil {
 		return nil, err
 	}
 	return &response.Profile, nil
-
 }
 
 // GetAccessLogs retrieves a page of logins according to the parameters given
@@ -192,6 +195,9 @@ func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogPar
 	values := url.Values{
 		"token": {api.token},
 	}
+	if params.TeamID != "" {
+		values.Add("team_id", params.TeamID)
+	}
 	if params.Count != DEFAULT_LOGINS_COUNT {
 		values.Add("count", strconv.Itoa(params.Count))
 	}
@@ -206,30 +212,28 @@ func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogPar
 	return response.Logins, &response.Paging, nil
 }
 
+type GetBillableInfoParams struct {
+	User   string
+	TeamID string
+}
+
 // GetBillableInfo ...
-func (api *Client) GetBillableInfo(user string) (map[string]BillingActive, error) {
-	return api.GetBillableInfoContext(context.Background(), user)
+func (api *Client) GetBillableInfo(params GetBillableInfoParams) (map[string]BillingActive, error) {
+	return api.GetBillableInfoContext(context.Background(), params)
 }
 
 // GetBillableInfoContext ...
-func (api *Client) GetBillableInfoContext(ctx context.Context, user string) (map[string]BillingActive, error) {
+func (api *Client) GetBillableInfoContext(ctx context.Context, params GetBillableInfoParams) (map[string]BillingActive, error) {
 	values := url.Values{
 		"token": {api.token},
-		"user":  {user},
 	}
 
-	return api.billableInfoRequest(ctx, "team.billableInfo", values)
-}
-
-// GetBillableInfoForTeam returns the billing_active status of all users on the team.
-func (api *Client) GetBillableInfoForTeam() (map[string]BillingActive, error) {
-	return api.GetBillableInfoForTeamContext(context.Background())
-}
+	if params.TeamID != "" {
+		values.Add("team_id", params.TeamID)
+	}
 
-// GetBillableInfoForTeamContext returns the billing_active status of all users on the team with a custom context
-func (api *Client) GetBillableInfoForTeamContext(ctx context.Context) (map[string]BillingActive, error) {
-	values := url.Values{
-		"token": {api.token},
+	if params.User != "" {
+		values.Add("user", params.User)
 	}
 
 	return api.billableInfoRequest(ctx, "team.billableInfo", values)
diff --git a/tokens.go b/tokens.go
new file mode 100644
index 0000000..4b83bee
--- /dev/null
+++ b/tokens.go
@@ -0,0 +1,50 @@
+package slack
+
+import (
+	"context"
+	"net/url"
+)
+
+// RotateTokens exchanges a refresh token for a new app configuration token
+func (api *Client) RotateTokens(configToken string, refreshToken string) (*TokenResponse, error) {
+	return api.RotateTokensContext(context.Background(), configToken, refreshToken)
+}
+
+// RotateTokensContext exchanges a refresh token for a new app configuration token with a custom context
+func (api *Client) RotateTokensContext(ctx context.Context, configToken string, refreshToken string) (*TokenResponse, error) {
+	if configToken == "" {
+		configToken = api.configToken
+	}
+
+	if refreshToken == "" {
+		refreshToken = api.configRefreshToken
+	}
+
+	values := url.Values{
+		"refresh_token": {refreshToken},
+	}
+
+	response := &TokenResponse{}
+	err := api.getMethod(ctx, "tooling.tokens.rotate", configToken, values, response)
+	if err != nil {
+		return nil, err
+	}
+
+	return response, response.Err()
+}
+
+// UpdateConfigTokens replaces the configuration tokens in the client with those returned by the API
+func (api *Client) UpdateConfigTokens(response *TokenResponse) {
+	api.configToken = response.Token
+	api.configRefreshToken = response.RefreshToken
+}
+
+type TokenResponse struct {
+	Token        string `json:"token,omitempty"`
+	RefreshToken string `json:"refresh_token,omitempty"`
+	TeamId       string `json:"team_id,omitempty"`
+	UserId       string `json:"user_id,omitempty"`
+	IssuedAt     uint64 `json:"iat,omitempty"`
+	ExpiresAt    uint64 `json:"exp,omitempty"`
+	SlackResponse
+}
diff --git a/usergroups.go b/usergroups.go
index c5d7a17..7244aba 100644
--- a/usergroups.go
+++ b/usergroups.go
@@ -62,6 +62,10 @@ func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGro
 		"name":  {userGroup.Name},
 	}
 
+	if userGroup.TeamID != "" {
+		values["team_id"] = []string{userGroup.TeamID}
+	}
+
 	if userGroup.Handle != "" {
 		values["handle"] = []string{userGroup.Handle}
 	}
@@ -122,6 +126,12 @@ func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string)
 // GetUserGroupsOption options for the GetUserGroups method call.
 type GetUserGroupsOption func(*GetUserGroupsParams)
 
+func GetUserGroupsOptionWithTeamID(teamID string) GetUserGroupsOption {
+	return func(params *GetUserGroupsParams) {
+		params.TeamID = teamID
+	}
+}
+
 // GetUserGroupsOptionIncludeCount include the number of users in each User Group (default: false)
 func GetUserGroupsOptionIncludeCount(b bool) GetUserGroupsOption {
 	return func(params *GetUserGroupsParams) {
@@ -145,6 +155,7 @@ func GetUserGroupsOptionIncludeUsers(b bool) GetUserGroupsOption {
 
 // GetUserGroupsParams contains arguments for GetUserGroups method call
 type GetUserGroupsParams struct {
+	TeamID          string
 	IncludeCount    bool
 	IncludeDisabled bool
 	IncludeUsers    bool
@@ -166,6 +177,9 @@ func (api *Client) GetUserGroupsContext(ctx context.Context, options ...GetUserG
 	values := url.Values{
 		"token": {api.token},
 	}
+	if params.TeamID != "" {
+		values.Add("team_id", params.TeamID)
+	}
 	if params.IncludeCount {
 		values.Add("include_count", "true")
 	}
diff --git a/users.go b/users.go
index a3b4c06..a3d32b2 100644
--- a/users.go
+++ b/users.go
@@ -17,31 +17,32 @@ const (
 
 // UserProfile contains all the information details of a given user
 type UserProfile struct {
-	FirstName              string                              `json:"first_name"`
-	LastName               string                              `json:"last_name"`
+	FirstName              string                              `json:"first_name,omitempty"`
+	LastName               string                              `json:"last_name,omitempty"`
 	RealName               string                              `json:"real_name"`
 	RealNameNormalized     string                              `json:"real_name_normalized"`
 	DisplayName            string                              `json:"display_name"`
 	DisplayNameNormalized  string                              `json:"display_name_normalized"`
-	Email                  string                              `json:"email"`
-	Skype                  string                              `json:"skype"`
-	Phone                  string                              `json:"phone"`
+	AvatarHash             string                              `json:"avatar_hash"`
+	Email                  string                              `json:"email,omitempty"`
+	Skype                  string                              `json:"skyp,omitempty"`
+	Phone                  string                              `json:"phone,omitempty"`
 	Image24                string                              `json:"image_24"`
 	Image32                string                              `json:"image_32"`
 	Image48                string                              `json:"image_48"`
 	Image72                string                              `json:"image_72"`
 	Image192               string                              `json:"image_192"`
 	Image512               string                              `json:"image_512"`
-	ImageOriginal          string                              `json:"image_original"`
-	Title                  string                              `json:"title"`
+	ImageOriginal          string                              `json:"image_original,omitempty"`
+	Title                  string                              `json:"title,omitempty"`
 	BotID                  string                              `json:"bot_id,omitempty"`
 	ApiAppID               string                              `json:"api_app_id,omitempty"`
 	StatusText             string                              `json:"status_text,omitempty"`
 	StatusEmoji            string                              `json:"status_emoji,omitempty"`
 	StatusEmojiDisplayInfo []UserProfileStatusEmojiDisplayInfo `json:"status_emoji_display_info,omitempty"`
-	StatusExpiration       int                                 `json:"status_expiration"`
+	StatusExpiration       int                                 `json:"status_expiration,omitempty"`
 	Team                   string                              `json:"team"`
-	Fields                 UserProfileCustomFields             `json:"fields"`
+	Fields                 UserProfileCustomFields             `json:"fields,omitempty"`
 }
 
 type UserProfileStatusEmojiDisplayInfo struct {
diff --git a/webhooks.go b/webhooks.go
index e323353..8fc149d 100644
--- a/webhooks.go
+++ b/webhooks.go
@@ -24,6 +24,8 @@ type WebhookMessage struct {
 	ReplaceOriginal bool         `json:"replace_original"`
 	DeleteOriginal  bool         `json:"delete_original"`
 	ReplyBroadcast  bool         `json:"reply_broadcast,omitempty"`
+	UnfurlLinks     bool         `json:"unfurl_links,omitempty"`
+	UnfurlMedia     bool         `json:"unfurl_media,omitempty"`
 }
 
 func PostWebhook(url string, msg *WebhookMessage) error {


Test patch:
diff --git a/block_element_test.go b/block_element_test.go
index 0c47aff..d532fb7 100644
--- a/block_element_test.go
+++ b/block_element_test.go
@@ -43,6 +43,16 @@ func TestWithStyleForButtonElement(t *testing.T) {
 
 }
 
+func TestWithURLForButtonElement(t *testing.T) {
+
+	btnTxt := NewTextBlockObject("plain_text", "Next 2 Results", false, false)
+	btnElement := NewButtonBlockElement("test", "click_me_123", btnTxt)
+
+	btnElement.WithURL("https://foo.bar")
+	assert.Equal(t, btnElement.URL, "https://foo.bar")
+
+}
+
 func TestNewOptionsSelectBlockElement(t *testing.T) {
 
 	testOptionText := NewTextBlockObject("plain_text", "Option One", false, false)
@@ -148,6 +158,12 @@ func TestNewPlainTextInputBlockElement(t *testing.T) {
 
 }
 
+func TestNewRichTextInputBlockElement(t *testing.T) {
+	richTextInputElement := NewRichTextInputBlockElement(nil, "test")
+	assert.Equal(t, string(richTextInputElement.Type), "rich_text_input")
+	assert.Equal(t, richTextInputElement.ActionID, "test")
+}
+
 func TestNewEmailTextInputBlockElement(t *testing.T) {
 	emailTextInputElement := NewEmailTextInputBlockElement(nil, "example@example.com")
 
@@ -215,3 +231,19 @@ func TestNewNumberInputBlockElement(t *testing.T) {
 	assert.Equal(t, numberInputElement.IsDecimalAllowed, true)
 
 }
+
+func TestNewFileInputBlockElement(t *testing.T) {
+
+	fileInputElement := NewFileInputBlockElement("test")
+
+	assert.Equal(t, string(fileInputElement.Type), "file_input")
+	assert.Equal(t, fileInputElement.ActionID, "test")
+
+	fileInputElement.WithFileTypes("jpg", "png")
+	assert.Equal(t, len(fileInputElement.FileTypes), 2)
+	assert.Contains(t, fileInputElement.FileTypes, "jpg")
+	assert.Contains(t, fileInputElement.FileTypes, "png")
+
+	fileInputElement.WithMaxFiles(10)
+	assert.Equal(t, fileInputElement.MaxFiles, 10)
+}
diff --git a/block_object_test.go b/block_object_test.go
index 9889fae..1f4874a 100644
--- a/block_object_test.go
+++ b/block_object_test.go
@@ -2,6 +2,7 @@ package slack
 
 import (
 	"errors"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -126,6 +127,24 @@ func TestValidateTextBlockObject(t *testing.T) {
 			},
 			expected: errors.New("emoji cannot be true in mrkdown"),
 		},
+		{
+			input: TextBlockObject{
+				Type:     "mrkdwn",
+				Text:     "",
+				Emoji:    false,
+				Verbatim: false,
+			},
+			expected: errors.New("text must have a minimum length of 1"),
+		},
+		{
+			input: TextBlockObject{
+				Type:     "mrkdwn",
+				Text:     strings.Repeat("a", 3001),
+				Emoji:    false,
+				Verbatim: false,
+			},
+			expected: errors.New("text cannot be longer than 3000 characters"),
+		},
 	}
 
 	for _, test := range tests {
diff --git a/block_rich_text_test.go b/block_rich_text_test.go
index 270dde9..a9f04b7 100644
--- a/block_rich_text_test.go
+++ b/block_rich_text_test.go
@@ -27,6 +27,60 @@ const (
     }
   ]
 }`
+
+	richTextQuotePayload = `{
+		"type": "rich_text",
+		"block_id": "G7G",
+		"elements": [
+			{
+				"type": "rich_text_section",
+				"elements": [
+					{
+						"type": "text",
+						"text": "Holy moly\n\n"
+					}
+				]
+			},
+			{
+				"type": "rich_text_preformatted",
+				"elements": [
+					{
+						"type": "text",
+						"text": "Preformatted\n\n"
+					}
+				],
+				"border": 2
+			},
+			{
+				"type": "rich_text_quote",
+				"elements": [
+					{
+						"type": "text",
+						"text": "Quote\n\n"
+					}
+				]
+			},
+			{
+				"type": "rich_text_quote",
+				"elements": [
+					{
+						"type": "text",
+						"text": "Another quote"
+					}
+				]
+			},
+			{
+				"type": "rich_text_preformatted",
+				"elements": [
+					{
+						"type": "text",
+						"text": "Another preformatted\n\n"
+					}
+				],
+				"border": 42
+			}
+		]
+	}`
 )
 
 func TestRichTextBlock_UnmarshalJSON(t *testing.T) {
@@ -36,11 +90,12 @@ func TestRichTextBlock_UnmarshalJSON(t *testing.T) {
 		err      error
 	}{
 		{
-			[]byte(`{"elements":[{"type":"rich_text_unknown"},{"type":"rich_text_section"}]}`),
+			[]byte(`{"elements":[{"type":"rich_text_unknown"},{"type":"rich_text_section"},{"type":"rich_text_list"}]}`),
 			RichTextBlock{
 				Elements: []RichTextElement{
 					&RichTextUnknown{Type: RTEUnknown, Raw: `{"type":"rich_text_unknown"}`},
 					&RichTextSection{Type: RTESection, Elements: []RichTextSectionElement{}},
+					&RichTextList{Type: RTEList, Elements: []RichTextElement{}},
 				},
 			},
 			nil,
@@ -54,6 +109,38 @@ func TestRichTextBlock_UnmarshalJSON(t *testing.T) {
 			},
 			nil,
 		},
+		{
+			[]byte(dummyPayload),
+			RichTextBlock{
+				Type:    MBTRichText,
+				BlockID: "FaYCD",
+				Elements: []RichTextElement{
+					&RichTextSection{
+						Type: RTESection,
+						Elements: []RichTextSectionElement{
+							&RichTextSectionChannelElement{Type: RTSEChannel, ChannelID: "C012345678"},
+							&RichTextSectionTextElement{Type: RTSEText, Text: "dummy_text"},
+						},
+					},
+				},
+			},
+			nil,
+		},
+		{
+			[]byte(richTextQuotePayload),
+			RichTextBlock{
+				Type:    MBTRichText,
+				BlockID: "G7G",
+				Elements: []RichTextElement{
+					&RichTextSection{Type: RTESection, Elements: []RichTextSectionElement{&RichTextSectionTextElement{Type: RTSEText, Text: "Holy moly\n\n"}}},
+					&RichTextPreformatted{RichTextSection: RichTextSection{Type: RTEPreformatted, Elements: []RichTextSectionElement{&RichTextSectionTextElement{Type: RTSEText, Text: "Preformatted\n\n"}}}, Border: 2},
+					&RichTextQuote{Type: RTEQuote, Elements: []RichTextSectionElement{&RichTextSectionTextElement{Type: RTSEText, Text: "Quote\n\n"}}},
+					&RichTextQuote{Type: RTEQuote, Elements: []RichTextSectionElement{&RichTextSectionTextElement{Type: RTSEText, Text: "Another quote"}}},
+					&RichTextPreformatted{RichTextSection: RichTextSection{Type: RTEPreformatted, Elements: []RichTextSectionElement{&RichTextSectionTextElement{Type: RTSEText, Text: "Another preformatted\n\n"}}}, Border: 42},
+				},
+			},
+			nil,
+		},
 	}
 	for _, tc := range cases {
 		var actual RichTextBlock
@@ -62,10 +149,10 @@ func TestRichTextBlock_UnmarshalJSON(t *testing.T) {
 			if tc.err == nil {
 				t.Errorf("unexpected error: %s", err)
 			}
-			t.Errorf("expected error is %s, but got %s", tc.err, err)
+			t.Errorf("expected error is %v, but got %v", tc.err, err)
 		}
 		if tc.err != nil {
-			t.Errorf("expected to raise an error %s", tc.err)
+			t.Errorf("expected to raise an error %v", tc.err)
 		}
 		if diff := deep.Equal(actual, tc.expected); diff != nil {
 			t.Errorf("actual value does not match expected one\n%s", diff)
@@ -117,3 +204,160 @@ func TestRichTextSection_UnmarshalJSON(t *testing.T) {
 		}
 	}
 }
+
+func TestRichTextList_UnmarshalJSON(t *testing.T) {
+	cases := []struct {
+		raw      []byte
+		expected RichTextList
+		err      error
+	}{
+		{
+			[]byte(`{"style":"ordered","elements":[{"type":"rich_text_unknown","value":10},{"type":"rich_text_section","elements":[{"type":"text","text":"hi"}]}]}`),
+			RichTextList{
+				Type:  RTEList,
+				Style: RTEListOrdered,
+				Elements: []RichTextElement{
+					&RichTextUnknown{Type: RTEUnknown, Raw: `{"type":"rich_text_unknown","value":10}`},
+					&RichTextSection{
+						Type: RTESection,
+						Elements: []RichTextSectionElement{
+							&RichTextSectionTextElement{Type: RTSEText, Text: "hi"},
+						},
+					},
+				},
+			},
+			nil,
+		},
+		{
+			[]byte(`{"style":"ordered","elements":[{"type":"rich_text_list","style":"bullet","elements":[{"type":"rich_text_section","elements":[{"type":"text","text":"hi"}]}]}]}`),
+			RichTextList{
+				Type:  RTEList,
+				Style: RTEListOrdered,
+				Elements: []RichTextElement{
+					&RichTextList{
+						Type:  RTEList,
+						Style: RTEListBullet,
+						Elements: []RichTextElement{
+							&RichTextSection{
+								Type: RTESection,
+								Elements: []RichTextSectionElement{
+									&RichTextSectionTextElement{Type: RTSEText, Text: "hi"},
+								},
+							},
+						},
+					},
+				},
+			},
+			nil,
+		},
+		{
+			[]byte(`{"type": "rich_text_list","elements":[]}`),
+			RichTextList{
+				Type:     RTEList,
+				Elements: []RichTextElement{},
+			},
+			nil,
+		},
+		{
+			[]byte(`{"type": "rich_text_list","elements":[],"indent":2}`),
+			RichTextList{
+				Type:     RTEList,
+				Indent:   2,
+				Elements: []RichTextElement{},
+			},
+			nil,
+		},
+	}
+	for _, tc := range cases {
+		var actual RichTextList
+		err := json.Unmarshal(tc.raw, &actual)
+		if err != nil {
+			if tc.err == nil {
+				t.Errorf("unexpected error: %s", err)
+			}
+			t.Errorf("expected error is %s, but got %s", tc.err, err)
+		}
+		if tc.err != nil {
+			t.Errorf("expected to raise an error %s", tc.err)
+		}
+		if diff := deep.Equal(actual, tc.expected); diff != nil {
+			t.Errorf("actual value does not match expected one\n%s", diff)
+		}
+	}
+}
+
+func TestRichTextQuote_Marshal(t *testing.T) {
+	t.Run("rich_text_section", func(t *testing.T) {
+		const rawRSE = "{\"type\":\"rich_text_section\",\"elements\":[{\"type\":\"text\",\"text\":\"Some Text\"}]}"
+
+		var got RichTextSection
+		if err := json.Unmarshal([]byte(rawRSE), &got); err != nil {
+			t.Fatal(err)
+		}
+		want := RichTextSection{
+			Type: RTESection,
+			Elements: []RichTextSectionElement{
+				&RichTextSectionTextElement{Type: RTSEText, Text: "Some Text"},
+			},
+		}
+
+		if diff := deep.Equal(got, want); diff != nil {
+			t.Errorf("actual value does not match expected one\n%s", diff)
+		}
+		b, err := json.Marshal(got)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if diff := deep.Equal(string(b), rawRSE); diff != nil {
+			t.Errorf("actual value does not match expected one\n%s", diff)
+		}
+	})
+	t.Run("rich_text_quote", func(t *testing.T) {
+		const rawRTS = "{\"type\":\"rich_text_quote\",\"elements\":[{\"type\":\"text\",\"text\":\"Some text\"}]}"
+
+		var got RichTextQuote
+		if err := json.Unmarshal([]byte(rawRTS), &got); err != nil {
+			t.Fatal(err)
+		}
+		want := RichTextQuote{
+			Type: RTEQuote,
+			Elements: []RichTextSectionElement{
+				&RichTextSectionTextElement{Type: RTSEText, Text: "Some text"},
+			},
+		}
+		if diff := deep.Equal(got, want); diff != nil {
+			t.Errorf("actual value does not match expected one\n%s", diff)
+		}
+		b, err := json.Marshal(got)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if diff := deep.Equal(string(b), rawRTS); diff != nil {
+			t.Errorf("actual value does not match expected one\n%s", diff)
+		}
+	})
+	t.Run("rich_text_preformatted", func(t *testing.T) {
+		const rawRTP = "{\"type\":\"rich_text_preformatted\",\"elements\":[{\"type\":\"text\",\"text\":\"Some other text\"}],\"border\":2}"
+		want := RichTextPreformatted{
+			RichTextSection: RichTextSection{
+				Type:     RTEPreformatted,
+				Elements: []RichTextSectionElement{&RichTextSectionTextElement{Type: RTSEText, Text: "Some other text"}},
+			},
+			Border: 2,
+		}
+		var got RichTextPreformatted
+		if err := json.Unmarshal([]byte(rawRTP), &got); err != nil {
+			t.Fatal(err)
+		}
+		if diff := deep.Equal(got, want); diff != nil {
+			t.Errorf("actual value does not match expected one\n%s", diff)
+		}
+		b, err := json.Marshal(got)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if diff := deep.Equal(string(b), rawRTP); diff != nil {
+			t.Errorf("actual value does not match expected one\n%s", diff)
+		}
+	})
+}
diff --git a/block_video_test.go b/block_video_test.go
new file mode 100644
index 0000000..d4b791f
--- /dev/null
+++ b/block_video_test.go
@@ -0,0 +1,23 @@
+package slack
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewVideoBlock(t *testing.T) {
+
+	videoTitle := NewTextBlockObject("plain_text", "VideoTitle", false, false)
+	videoBlock := NewVideoBlock(
+		"https://example.com/example.mp4",
+		"https://example.com/thumbnail.png",
+		"alternative text", "blockID", videoTitle)
+
+	assert.Equal(t, string(videoBlock.Type), "video")
+	assert.Equal(t, videoBlock.Title.Type, "plain_text")
+	assert.Equal(t, videoBlock.BlockID, "blockID")
+	assert.Contains(t, videoBlock.Title.Text, "VideoTitle")
+	assert.Contains(t, videoBlock.VideoURL, "example.mp4")
+
+}
diff --git a/bots_test.go b/bots_test.go
index ce7f668..14a509e 100644
--- a/bots_test.go
+++ b/bots_test.go
@@ -29,7 +29,7 @@ func TestGetBotInfo(t *testing.T) {
 	once.Do(startServer)
 	api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
 
-	bot, err := api.GetBotInfo("B02875YLA")
+	bot, err := api.GetBotInfo(GetBotInfoParameters{Bot: "B02875YLA"})
 	if err != nil {
 		t.Errorf("Unexpected error: %s", err)
 		return
diff --git a/chat_test.go b/chat_test.go
index 7a549fa..ee93bdd 100644
--- a/chat_test.go
+++ b/chat_test.go
@@ -1,11 +1,14 @@
 package slack
 
 import (
+	"bytes"
 	"encoding/json"
 	"io/ioutil"
+	"log"
 	"net/http"
 	"net/url"
 	"reflect"
+	"regexp"
 	"testing"
 )
 
@@ -39,7 +42,6 @@ func TestGetPermalink(t *testing.T) {
 	timeStamp := "p135854651500008"
 
 	http.HandleFunc("/chat.getPermalink", func(rw http.ResponseWriter, r *http.Request) {
-
 		if got, want := r.Header.Get("Content-Type"), "application/x-www-form-urlencoded"; got != want {
 			t.Errorf("request uses unexpected content type: got %s, want %s", got, want)
 		}
@@ -183,6 +185,28 @@ func TestPostMessage(t *testing.T) {
 				"user_auth_message": []string{"Please!"},
 			},
 		},
+		"LinkNames true": {
+			endpoint: "/chat.postMessage",
+			opt: []MsgOption{
+				MsgOptionLinkNames(true),
+			},
+			expected: url.Values{
+				"channel":    []string{"CXXX"},
+				"token":      []string{"testing-token"},
+				"link_names": []string{"true"},
+			},
+		},
+		"LinkNames false": {
+			endpoint: "/chat.postMessage",
+			opt: []MsgOption{
+				MsgOptionLinkNames(false),
+			},
+			expected: url.Values{
+				"channel":    []string{"CXXX"},
+				"token":      []string{"testing-token"},
+				"link_names": []string{"false"},
+			},
+		},
 	}
 
 	once.Do(startServer)
@@ -296,3 +320,50 @@ func TestPostMessageWhenMsgOptionDeleteOriginalApplied(t *testing.T) {
 
 	_, _, _ = api.PostMessage("CXXX", MsgOptionDeleteOriginal(responseURL))
 }
+
+func TestSendMessageContextRedactsTokenInDebugLog(t *testing.T) {
+	tests := []struct {
+		name  string
+		token string
+		want  string
+	}{
+		{
+			name:  "regular token",
+			token: "xtest-token-1234-abcd",
+			want:  "xtest-REDACTED",
+		},
+		{
+			name:  "refresh token",
+			token: "xoxe.xtest-token-1234-abcd",
+			want:  "xoxe.xtest-REDACTED",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			once.Do(startServer)
+			buf := bytes.NewBufferString("")
+
+			opts := []Option{
+				OptionAPIURL("http://" + serverAddr + "/"),
+				OptionLog(log.New(buf, "", log.Lshortfile)),
+				OptionDebug(true),
+			}
+			api := New(tt.token, opts...)
+			// Why send the token in the message text too? To test that we're not
+			// redacting substrings in the request which look like a token but aren't.
+			api.SendMessage("CXXX", MsgOptionText(token, false))
+			s := buf.String()
+
+			re := regexp.MustCompile(`token=[\w.-]*`)
+			want := "token=" + tt.want
+			if got := re.FindString(s); got != want {
+				t.Errorf("Logged token in SendMessageContext(): got %q, want %q", got, want)
+			}
+			re = regexp.MustCompile(`text=[\w.-]*`)
+			want = "text=" + token
+			if got := re.FindString(s); got != want {
+				t.Errorf("Logged text in SendMessageContext(): got %q, want %q", got, want)
+			}
+		})
+	}
+}
diff --git a/conversation_test.go b/conversation_test.go
index 85011d5..1c6b8a2 100644
--- a/conversation_test.go
+++ b/conversation_test.go
@@ -287,6 +287,20 @@ func okChannelJsonHandler(rw http.ResponseWriter, r *http.Request) {
 	rw.Write(response)
 }
 
+func okInviteSharedJsonHandler(rw http.ResponseWriter, r *http.Request) {
+	rw.Header().Set("Content-Type", "application/json")
+	response, _ := json.Marshal(struct {
+		SlackResponse
+		InviteID              string `json:"invite_id"`
+		IsLegacySharedChannel bool   `json:"is_legacy_shared_channel"`
+	}{
+		SlackResponse:         SlackResponse{Ok: true},
+		InviteID:              "I01234567",
+		IsLegacySharedChannel: false,
+	})
+	rw.Write(response)
+}
+
 func TestSetTopicOfConversation(t *testing.T) {
 	http.HandleFunc("/conversations.setTopic", okChannelJsonHandler)
 	once.Do(startServer)
@@ -348,6 +362,44 @@ func TestInviteUsersToConversation(t *testing.T) {
 	}
 }
 
+func TestInviteSharedToConversation(t *testing.T) {
+	http.HandleFunc("/conversations.inviteShared", okInviteSharedJsonHandler)
+	once.Do(startServer)
+	api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
+
+	t.Run("user_ids", func(t *testing.T) {
+		userIDs := []string{"UXXXXXXX1", "UXXXXXXX2"}
+		inviteID, isLegacySharedChannel, err := api.InviteSharedUserIDsToConversation("CXXXXXXXX", userIDs...)
+		if err != nil {
+			t.Errorf("Unexpected error: %s", err)
+			return
+		}
+		if inviteID == "" {
+			t.Error("invite id should have a value")
+			return
+		}
+		if isLegacySharedChannel {
+			t.Error("is legacy shared channel should be false")
+		}
+	})
+
+	t.Run("emails", func(t *testing.T) {
+		emails := []string{"nopcoder@slack.com", "nopcoder@example.com"}
+		inviteID, isLegacySharedChannel, err := api.InviteSharedEmailsToConversation("CXXXXXXXX", emails...)
+		if err != nil {
+			t.Errorf("Unexpected error: %s", err)
+			return
+		}
+		if inviteID == "" {
+			t.Error("invite id should have a value")
+			return
+		}
+		if isLegacySharedChannel {
+			t.Error("is legacy shared channel should be false")
+		}
+	})
+}
+
 func TestKickUserFromConversation(t *testing.T) {
 	http.HandleFunc("/conversations.kick", okJSONHandler)
 	once.Do(startServer)
diff --git a/interactions_test.go b/interactions_test.go
index f492b1f..989690e 100644
--- a/interactions_test.go
+++ b/interactions_test.go
@@ -286,14 +286,14 @@ func TestViewSubmissionCallback(t *testing.T) {
 			},
 			State: &ViewState{
 				Values: map[string]map[string]BlockAction{
-					"multi-line": map[string]BlockAction{
-						"ml-value": BlockAction{
+					"multi-line": {
+						"ml-value": {
 							Type:  "plain_text_input",
 							Value: "No onions",
 						},
 					},
-					"target_channel": map[string]BlockAction{
-						"target_select": BlockAction{
+					"target_channel": {
+						"target_select": {
 							Type:  "conversations_select",
 							Value: "C1AB2C3DE",
 						},
diff --git a/manifests_test.go b/manifests_test.go
new file mode 100644
index 0000000..9133e68
--- /dev/null
+++ b/manifests_test.go
@@ -0,0 +1,149 @@
+package slack
+
+import (
+	"encoding/json"
+	"net/http"
+	"reflect"
+	"testing"
+)
+
+func TestCreateManifest(t *testing.T) {
+	http.HandleFunc("/apps.manifest.create", handleCreateManifest)
+	once.Do(startServer)
+
+	api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
+
+	manif := getTestManifest()
+	resp, err := api.CreateManifest(&manif, "token")
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+		return
+	}
+
+	if !reflect.DeepEqual(resp, getTestManifestResponse()) {
+		t.Fatal(ErrIncorrectResponse)
+	}
+}
+
+func handleCreateManifest(rw http.ResponseWriter, r *http.Request) {
+	rw.Header().Set("Content-Type", "application/json")
+
+	response, _ := json.Marshal(getTestManifestResponse())
+	rw.Write(response)
+}
+
+func TestDeleteManifest(t *testing.T) {
+	http.HandleFunc("/apps.manifest.delete", handleDeleteManifest)
+	expectedResponse := SlackResponse{Ok: true}
+
+	once.Do(startServer)
+	api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
+
+	resp, err := api.DeleteManifest("token", "app id")
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+		return
+	}
+
+	if !reflect.DeepEqual(expectedResponse, *resp) {
+		t.Fatal(ErrIncorrectResponse)
+	}
+}
+
+func handleDeleteManifest(rw http.ResponseWriter, r *http.Request) {
+	rw.Header().Set("Content-Type", "application/json")
+
+	response, _ := json.Marshal(SlackResponse{Ok: true})
+	rw.Write(response)
+}
+
+func TestExportManifest(t *testing.T) {
+	http.HandleFunc("/apps.manifest.export", handleExportManifest)
+	expectedResponse := getTestManifest()
+
+	api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
+
+	resp, err := api.ExportManifest("token", "app id")
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+		return
+	}
+
+	if !reflect.DeepEqual(expectedResponse, *resp) {
+		t.Fatal(ErrIncorrectResponse)
+	}
+}
+
+func handleExportManifest(rw http.ResponseWriter, r *http.Request) {
+	rw.Header().Set("Content-Type", "application/json")
+
+	response, _ := json.Marshal(ExportManifestResponse{Manifest: getTestManifest()})
+	rw.Write(response)
+}
+
+func TestUpdateManifest(t *testing.T) {
+	http.HandleFunc("/apps.manifest.update", handleUpdateManifest)
+	expectedResponse := UpdateManifestResponse{AppId: "app id"}
+
+	api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
+
+	manif := getTestManifest()
+	resp, err := api.UpdateManifest(&manif, "token", "app id")
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+		return
+	}
+
+	if !reflect.DeepEqual(expectedResponse, *resp) {
+		t.Fatal(ErrIncorrectResponse)
+	}
+}
+
+func handleUpdateManifest(rw http.ResponseWriter, r *http.Request) {
+	rw.Header().Set("Content-Type", "application/json")
+
+	response, _ := json.Marshal(UpdateManifestResponse{AppId: "app id"})
+	rw.Write(response)
+}
+
+func TestValidateManifest(t *testing.T) {
+	http.HandleFunc("/apps.manifest.validate", handleValidateManifest)
+	expectedResponse := ManifestResponse{SlackResponse: SlackResponse{Ok: true}}
+
+	api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
+
+	manif := getTestManifest()
+	resp, err := api.ValidateManifest(&manif, "token", "app id")
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+		return
+	}
+
+	if !reflect.DeepEqual(expectedResponse, *resp) {
+		t.Fatal(ErrIncorrectResponse)
+	}
+}
+
+func handleValidateManifest(rw http.ResponseWriter, r *http.Request) {
+	rw.Header().Set("Content-Type", "application/json")
+
+	response, _ := json.Marshal(ManifestResponse{SlackResponse: SlackResponse{Ok: true}})
+	rw.Write(response)
+}
+
+func getTestManifest() Manifest {
+	return Manifest{
+		Display: Display{
+			Name:        "test",
+			Description: "this is a test",
+		},
+	}
+}
+
+func getTestManifestResponse() *ManifestResponse {
+	return &ManifestResponse{
+		SlackResponse: SlackResponse{
+			Ok: true,
+		},
+	}
+}
diff --git a/slackevents/inner_events_test.go b/slackevents/inner_events_test.go
index f21f66b..20b8bff 100644
--- a/slackevents/inner_events_test.go
+++ b/slackevents/inner_events_test.go
@@ -2,7 +2,10 @@ package slackevents
 
 import (
 	"encoding/json"
+	"fmt"
 	"testing"
+
+	"github.com/stretchr/testify/assert"
 )
 
 func TestAppMention(t *testing.T) {
@@ -376,19 +379,31 @@ func TestThreadBroadcastEvent(t *testing.T) {
 
 func TestMemberJoinedChannelEvent(t *testing.T) {
 	rawE := []byte(`
-			{
-				"type": "member_joined_channel",
-				"user": "W06GH7XHN",
-				"channel": "C0698JE0H",
-				"channel_type": "C",
-				"team": "T024BE7LD",
-				"inviter": "U123456789"
+		{
+			"type": "member_joined_channel",
+			"user": "W06GH7XHN",
+			"channel": "C0698JE0H",
+			"channel_type": "C",
+			"team": "T024BE7LD",
+			"inviter": "U123456789"
 		}
 	`)
-	err := json.Unmarshal(rawE, &MemberJoinedChannelEvent{})
+	evt := MemberJoinedChannelEvent{}
+	err := json.Unmarshal(rawE, &evt)
 	if err != nil {
 		t.Error(err)
 	}
+
+	expected := MemberJoinedChannelEvent{
+		Type:        "member_joined_channel",
+		User:        "W06GH7XHN",
+		Channel:     "C0698JE0H",
+		ChannelType: "C",
+		Team:        "T024BE7LD",
+		Inviter:     "U123456789",
+	}
+
+	assert.Equal(t, expected, evt)
 }
 
 func TestMemberLeftChannelEvent(t *testing.T) {
@@ -914,3 +929,572 @@ func TestUserProfileChanged(t *testing.T) {
 		t.Fail()
 	}
 }
+
+func TestSharedChannelInvite(t *testing.T) {
+	rawE := []byte(`
+	{
+		"token": "whatever",
+		"team_id": "whatever",
+		"api_app_id": "whatever",
+		"event": {
+			"type": "shared_channel_invite_received",
+			"invite": {
+				"id": "I028YDERZSQ",
+				"date_created": 1626876000,
+				"date_invalid": 1628085600,
+				"inviting_team": {
+					"id": "T12345678",
+					"name": "Corgis",
+					"icon": {},
+					"is_verified": false,
+					"domain": "corgis",
+					"date_created": 1480946400
+				},
+				"inviting_user": {
+					"id": "U12345678",
+					"team_id": "T12345678",
+					"name": "crus",
+					"updated": 1608081902,
+					"profile": {
+						"real_name": "Corgis Rus",
+						"display_name": "Corgis Rus",
+						"real_name_normalized": "Corgis Rus",
+						"display_name_normalized": "Corgis Rus",
+						"team": "T12345678",
+						"avatar_hash": "gcfh83a4c72k",
+						"email": "corgisrus@slack-corp.com",
+						"image_24": "https://placekitten.com/24/24",
+						"image_32": "https://placekitten.com/32/32",
+						"image_48": "https://placekitten.com/48/48",
+						"image_72": "https://placekitten.com/72/72",
+						"image_192": "https://placekitten.com/192/192",
+						"image_512": "https://placekitten.com/512/512"
+					}
+				},
+				"recipient_user_id": "U87654321"
+			},
+			"channel": {
+				"id": "C12345678",
+				"is_private": false,
+				"is_im": false,
+				"name": "test-slack-connect"
+			},
+			"event_ts": "1626876010.000100"
+		}
+	}
+	`)
+
+	evt := &EventsAPICallbackEvent{}
+	err := json.Unmarshal(rawE, evt)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	parsedEvent, err := parseInnerEvent(evt)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	actual, ok := parsedEvent.InnerEvent.Data.(*SharedChannelInviteReceivedEvent)
+	if !ok {
+		t.Fail()
+	}
+
+	if actual.Invite.ID != "I028YDERZSQ" {
+		t.Fail()
+	}
+
+	if actual.Invite.InvitingTeam.ID != "T12345678" {
+		t.Fail()
+	}
+
+	if actual.Invite.InvitingUser.ID != "U12345678" {
+		t.Fail()
+	}
+
+	if actual.Invite.RecipientUserID != "U87654321" {
+		t.Fail()
+	}
+
+	if actual.Channel.ID != "C12345678" {
+		t.Fail()
+	}
+
+	if parsedEvent.InnerEvent.Type != "shared_channel_invite_received" {
+		t.Fail()
+	}
+
+}
+
+// Test that the shared_channel_invite_accepted event can be unmarshalled
+func TestSharedChannelAccepted(t *testing.T) {
+	rawE := []byte(`
+	{
+		"token": "whatever",
+		"team_id": "whatever",
+		"api_app_id": "whatever",
+		"event": {
+			"type": "shared_channel_invite_accepted",
+			"approval_required": false,
+			"invite": {
+				"id": "I028YDERZSQ",
+				"date_created": 1626876000,
+				"date_invalid": 1628085600,
+				"inviting_team": {
+					"id": "T12345678",
+					"name": "Corgis",
+					"icon": {
+						"image_default": true,
+						"image_34": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-34.png",
+						"image_44": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-44.png",
+						"image_68": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-68.png",
+						"image_88": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-88.png",
+						"image_102": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-102.png",
+						"image_230": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-230.png",
+						"image_132": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-132.png"
+					  },
+					"is_verified": false,
+					"domain": "corgis",
+					"date_created": 1480946400
+				},
+				"inviting_user": {
+					"id": "U12345678",
+					"team_id": "T12345678",
+					"name": "crus",
+					"updated": 1608081902,
+					"profile": {
+						"real_name": "Corgis Rus",
+						"display_name": "Corgis Rus",
+						"real_name_normalized": "Corgis Rus",
+						"display_name_normalized": "Corgis Rus",
+						"team": "T12345678",
+						"avatar_hash": "gcfh83a4c72k",
+						"email": "corgisrus@slack-corp.com",
+						"image_24": "https://placekitten.com/24/24",
+						"image_32": "https://placekitten.com/32/32",
+						"image_48": "https://placekitten.com/48/48",
+						"image_72": "https://placekitten.com/72/72",
+						"image_192": "https://placekitten.com/192/192",
+						"image_512": "https://placekitten.com/512/512"
+					}
+				},
+				"recipient_email": "golden@doodle.com",
+				"recipient_user_id": "U87654321"
+			},
+			"channel": {
+				"id": "C12345678",
+				"is_private": false,
+				"is_im": false,
+				"name": "test-slack-connect"
+			},
+			"teams_in_channel": [
+				{
+				"id": "T12345678",
+				"name": "Corgis",
+				"icon": {
+					"image_default": true,
+					"image_34": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-34.png",
+					"image_44": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-44.png",
+					"image_68": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-68.png",
+					"image_88": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-88.png",
+					"image_102": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-102.png",
+					"image_230": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-230.png",
+					"image_132": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-132.png"
+				  },
+				"is_verified": false,
+				"domain": "corgis",
+				"date_created": 1626789600
+				}
+			],
+			"accepting_user": {
+				"id": "U87654321",
+				"team_id": "T87654321",
+				"name": "golden",
+				"updated": 1624406113,
+				"profile": {
+					"real_name": "Golden Doodle",
+					"display_name": "Golden",
+					"real_name_normalized": "Golden Doodle",
+					"display_name_normalized": "Golden",
+					"team": "T87654321",
+					"avatar_hash": "g717728b118x",
+					"email": "golden@doodle.com",
+					"image_24": "https://placekitten.com/24/24",
+					"image_32": "https://placekitten.com/32/32",
+					"image_48": "https://placekitten.com/48/48",
+					"image_72": "https://placekitten.com/72/72",
+					"image_192": "https://placekitten.com/192/192",
+					"image_512": "https://placekitten.com/512/512"
+				}
+			},
+			"event_ts": "1626877800.000000"
+		}
+	}
+	`)
+
+	evt := &EventsAPICallbackEvent{}
+	err := json.Unmarshal(rawE, evt)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	parsedEvent, err := parseInnerEvent(evt)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	actual, ok := parsedEvent.InnerEvent.Data.(*SharedChannelInviteAcceptedEvent)
+	if !ok {
+		t.Fail()
+	}
+
+	if actual.Invite.ID != "I028YDERZSQ" {
+		t.Fail()
+	}
+
+	if actual.Invite.InvitingTeam.ID != "T12345678" {
+		t.Fail()
+	}
+
+	if actual.Invite.InvitingUser.ID != "U12345678" {
+		t.Fail()
+	}
+
+	if actual.Invite.RecipientUserID != "U87654321" {
+		t.Fail()
+	}
+
+	if actual.Channel.ID != "C12345678" {
+		t.Fail()
+	}
+
+	if actual.Channel.Name != "test-slack-connect" {
+		t.Fail()
+		fmt.Println(actual.Channel.Name + ", does not match the test name.")
+	}
+
+	if actual.AcceptingUser.ID != "U87654321" {
+		t.Fail()
+	}
+
+	if actual.AcceptingUser.Profile.RealName != "Golden Doodle" {
+		t.Fail()
+	}
+
+	if parsedEvent.InnerEvent.Type != "shared_channel_invite_accepted" {
+		t.Fail()
+	}
+
+}
+
+// Test that the shared_channel_invite_declined event can be unmarshalled
+func TestSharedChannelApproved(t *testing.T) {
+	rawE := []byte(`
+	{
+		"token": "whatever",
+		"team_id": "whatever",
+		"api_app_id": "whatever",
+		"event": {
+			"type": "shared_channel_invite_approved",
+			"invite": {
+				"id": "I01354X80CA",
+				"date_created": 1626876000,
+				"date_invalid": 1628085600,
+				"inviting_team": {
+					"id": "T12345678",
+					"name": "Corgis",
+					"icon": {
+						"image_default": true,
+						"image_34": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-34.png",
+						"image_44": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-44.png",
+						"image_68": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-68.png",
+						"image_88": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-88.png",
+						"image_102": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-102.png",
+						"image_230": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-230.png",
+						"image_132": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-132.png"
+					  },
+					"is_verified": false,
+					"domain": "corgis",
+					"date_created": 1480946400
+				},
+				"inviting_user": {
+					"id": "U12345678",
+					"team_id": "T12345678",
+					"name": "crus",
+					"updated": 1608081902,
+					"profile": {
+						"real_name": "Corgis Rus",
+						"display_name": "Corgis Rus",
+						"real_name_normalized": "Corgis Rus",
+						"display_name_normalized": "Corgis Rus",
+						"team": "T12345678",
+						"avatar_hash": "gcfh83a4c72k",
+						"email": "corgisrus@slack-corp.com",
+						"image_24": "https://placekitten.com/24/24",
+						"image_32": "https://placekitten.com/32/32",
+						"image_48": "https://placekitten.com/48/48",
+						"image_72": "https://placekitten.com/72/72",
+						"image_192": "https://placekitten.com/192/192",
+						"image_512": "https://placekitten.com/512/512"
+					}
+				},
+				"recipient_email": "golden@doodle.com",
+				"recipient_user_id": "U87654321"
+			},
+			"channel": {
+				"id": "C12345678",
+				"is_private": false,
+				"is_im": false,
+				"name": "test-slack-connect"
+			},
+			"approving_team_id": "T87654321",
+			"teams_in_channel": [
+				{
+				"id": "T12345678",
+				"name": "Corgis",
+				"icon": {
+					"image_default": true,
+					"image_34": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-34.png",
+					"image_44": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-44.png",
+					"image_68": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-68.png",
+					"image_88": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-88.png",
+					"image_102": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-102.png",
+					"image_230": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-230.png",
+					"image_132": "https://a.slack-edge.com/80588/img/avatars-teams/ava_0011-132.png"
+				  },
+				"is_verified": false,
+				"domain": "corgis",
+				"date_created": 1626789600
+				}
+			],
+			"approving_user": {
+				"id": "U012A3CDE",
+				"team_id": "T87654321",
+				"name": "spengler",
+				"updated": 1624406532,
+				"profile": {
+					"real_name": "Egon Spengler",
+					"display_name": "Egon",
+					"real_name_normalized": "Egon Spengler",
+					"display_name_normalized": "Egon",
+					"team": "T87654321",
+					"avatar_hash": "g216425b1681",
+					"email": "spengler@ghostbusters.example.com",
+					"image_24": "https://placekitten.com/24/24",
+					"image_32": "https://placekitten.com/32/32",
+					"image_48": "https://placekitten.com/48/48",
+					"image_72": "https://placekitten.com/72/72",
+					"image_192": "https://placekitten.com/192/192",
+					"image_512": "https://placekitten.com/512/512"
+				}
+			},
+			"event_ts": "1626881400.000000"
+		}
+	}
+	`)
+
+	evt := &EventsAPICallbackEvent{}
+	err := json.Unmarshal(rawE, evt)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	parsedEvent, err := parseInnerEvent(evt)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	actual, ok := parsedEvent.InnerEvent.Data.(*SharedChannelInviteApprovedEvent)
+	if !ok {
+		t.Fail()
+	}
+
+	if actual.Invite.ID != "I01354X80CA" {
+		t.Fail()
+	}
+
+	if actual.Invite.InvitingTeam.ID != "T12345678" {
+		t.Fail()
+	}
+
+	if actual.Invite.InvitingUser.ID != "U12345678" {
+		t.Fail()
+	}
+
+	if actual.Invite.RecipientUserID != "U87654321" {
+		t.Fail()
+	}
+
+	if actual.Channel.ID != "C12345678" {
+		t.Fail()
+	}
+
+	if actual.ApprovingTeamID != "T87654321" {
+		t.Fail()
+	}
+
+	if actual.ApprovingUser.Name != "spengler" {
+		t.Fail()
+	}
+
+	if actual.ApprovingUser.Profile.RealName != "Egon Spengler" {
+		t.Fail()
+	}
+
+	if actual.TeamsInChannel[0].ID != "T12345678" {
+		t.Fail()
+	}
+
+	if parsedEvent.InnerEvent.Type != "shared_channel_invite_approved" {
+		t.Fail()
+	}
+
+}
+
+func TestSharedChannelDeclined(t *testing.T) {
+	rawE := []byte(`
+	{
+		"token": "whatever",
+		"team_id": "whatever",
+		"api_app_id": "whatever",
+		"event": {
+			"type": "shared_channel_invite_declined",
+			"invite": {
+				"id": "I01354X80CA",
+				"date_created": 1626876000,
+				"date_invalid": 1628085600,
+				"inviting_team": {
+					"id": "T12345678",
+					"name": "Corgis",
+					"icon": {},
+					"is_verified": false,
+					"domain": "corgis",
+					"date_created": 1480946400
+				},
+				"inviting_user": {
+					"id": "U12345678",
+					"team_id": "T12345678",
+					"name": "crus",
+					"updated": 1608081902,
+					"profile": {
+						"real_name": "Corgis Rus",
+						"display_name": "Corgis Rus",
+						"real_name_normalized": "Corgis Rus",
+						"display_name_normalized": "Corgis Rus",
+						"team": "T12345678",
+						"avatar_hash": "gcfh83a4c72k",
+						"email": "corgisrus@slack-corp.com",
+						"image_24": "https://placekitten.com/24/24",
+						"image_32": "https://placekitten.com/32/32",
+						"image_48": "https://placekitten.com/48/48",
+						"image_72": "https://placekitten.com/72/72",
+						"image_192": "https://placekitten.com/192/192",
+						"image_512": "https://placekitten.com/512/512"
+					}
+				},
+				"recipient_email": "golden@doodle.com"
+			},
+			"channel": {
+				"id": "C12345678",
+				"is_private": false,
+				"is_im": false,
+				"name": "test-slack-connect"
+			},
+			"declining_team_id": "T87654321",
+			"teams_in_channel": [
+				{
+					"id": "T12345678",
+					"name": "Corgis",
+					"icon": {},
+					"is_verified": false,
+					"domain": "corgis",
+					"date_created": 1626789600
+				}
+			],
+			"declining_user": {
+				"id": "U012A3CDE",
+				"team_id": "T87654321",
+				"name": "spengler",
+				"updated": 1624406532,
+					"profile": {
+					"real_name": "Egon Spengler",
+					"display_name": "Egon",
+					"real_name_normalized": "Egon Spengler",
+					"display_name_normalized": "Egon",
+					"team": "T87654321",
+					"avatar_hash": "g216425b1681",
+					"email": "spengler@ghostbusters.example.com",
+					"image_24": "https://placekitten.com/24/24",
+					"image_32": "https://placekitten.com/32/32",
+					"image_48": "https://placekitten.com/48/48",
+					"image_72": "https://placekitten.com/72/72",
+					"image_192": "https://placekitten.com/192/192",
+					"image_512": "https://placekitten.com/512/512"
+				}
+			},
+			"event_ts": "1626881400.000000"
+		}
+	}
+	`)
+
+	evt := &EventsAPICallbackEvent{}
+	err := json.Unmarshal(rawE, evt)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	parsedEvent, err := parseInnerEvent(evt)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	actual, ok := parsedEvent.InnerEvent.Data.(*SharedChannelInviteDeclinedEvent)
+	if !ok {
+		t.Fail()
+	}
+
+	if actual.Invite.ID != "I01354X80CA" {
+		t.Fail()
+	}
+
+	if actual.Invite.InvitingTeam.ID != "T12345678" {
+		t.Fail()
+	}
+
+	if actual.Invite.InvitingUser.ID != "U12345678" {
+		t.Fail()
+	}
+
+	if actual.Invite.RecipientEmail != "golden@doodle.com" {
+		t.Fail()
+	}
+
+	if actual.Channel.ID != "C12345678" {
+		t.Fail()
+	}
+
+	if actual.DecliningTeamID != "T87654321" {
+		t.Fail()
+	}
+
+	if actual.DecliningUser.Name != "spengler" {
+		t.Fail()
+	}
+
+	if actual.DecliningUser.Profile.RealName != "Egon Spengler" {
+		t.Fail()
+	}
+
+	if actual.TeamsInChannel[0].ID != "T12345678" {
+		t.Fail()
+	}
+
+	if actual.EventTs != "1626881400.000000" {
+		t.Fail()
+	}
+
+	if parsedEvent.InnerEvent.Type != "shared_channel_invite_declined" {
+		t.Fail()
+	}
+
+}
diff --git a/slacktest/data.go b/slacktest/data.go
index e33b901..31d9a08 100644
--- a/slacktest/data.go
+++ b/slacktest/data.go
@@ -35,11 +35,11 @@ var okWebResponse = slack.SlackResponse{
 	Ok: true,
 }
 
-var defaultOkJSON = fmt.Sprintf(`
+var defaultOkJSON = `
 	{
 		"ok": true
 	}
-	`)
+	`
 
 var defaultChannelsListJSON = fmt.Sprintf(`
 	{
@@ -250,3 +250,9 @@ var renameConversationJSON = fmt.Sprintf(templateConversationJSON, "newName",
 
 var inviteConversationJSON = fmt.Sprintf(templateConversationJSON, defaultConversationName,
 	nowAsJSONTime(), defaultBotID, defaultConversationName, "", "", 0, "", "", 0, 1)
+
+const inviteSharedResponseJSON = `{
+    "ok": true,
+    "invite_id": "I02UKAJ6RJA",
+    "is_legacy_shared_channel": false
+}`
diff --git a/slacktest/handlers.go b/slacktest/handlers.go
index 6ac37c2..b4a0227 100644
--- a/slacktest/handlers.go
+++ b/slacktest/handlers.go
@@ -108,6 +108,11 @@ func inviteConversationHandler(w http.ResponseWriter, r *http.Request) {
 	_, _ = w.Write([]byte(inviteConversationJSON))
 }
 
+// handle conversations.inviteShared
+func inviteSharedConversationHandler(w http.ResponseWriter, r *http.Request) {
+	_, _ = w.Write([]byte(inviteSharedResponseJSON))
+}
+
 // handle groups.list
 func listGroupsHandler(w http.ResponseWriter, _ *http.Request) {
 	_, _ = w.Write([]byte(defaultGroupsListJSON))
diff --git a/slacktest/handlers_test.go b/slacktest/handlers_test.go
index dec4c99..8ccb704 100644
--- a/slacktest/handlers_test.go
+++ b/slacktest/handlers_test.go
@@ -117,7 +117,7 @@ func TestBotInfoHandler(t *testing.T) {
 	go s.Start()
 
 	client := slack.New("ABCDEFG", slack.OptionAPIURL(s.GetAPIURL()))
-	bot, err := client.GetBotInfo(s.BotID)
+	bot, err := client.GetBotInfo(slack.GetBotInfoParameters{Bot: s.BotID})
 	assert.NoError(t, err)
 	assert.Equal(t, s.BotID, bot.ID)
 	assert.Equal(t, s.BotName, bot.Name)
diff --git a/slacktest/server.go b/slacktest/server.go
index 6470310..6d98494 100644
--- a/slacktest/server.go
+++ b/slacktest/server.go
@@ -55,6 +55,7 @@ func NewTestServer(custom ...Binder) *Server {
 	s.Handle("/conversations.setPurpose", setConversationPurposeHandler)
 	s.Handle("/conversations.rename", renameConversationHandler)
 	s.Handle("/conversations.invite", inviteConversationHandler)
+	s.Handle("/conversations.inviteShared", inviteSharedConversationHandler)
 	s.Handle("/users.info", usersInfoHandler)
 	s.Handle("/users.lookupByEmail", usersInfoHandler)
 	s.Handle("/bots.info", botsInfoHandler)
diff --git a/slacktest/server_test.go b/slacktest/server_test.go
index ac42444..45f6208 100644
--- a/slacktest/server_test.go
+++ b/slacktest/server_test.go
@@ -48,7 +48,7 @@ func TestBotDirectMessageBotHandler(t *testing.T) {
 	s := NewTestServer()
 	go s.Start()
 	s.SendDirectMessageToBot("some text")
-	expectedMsg := fmt.Sprintf("some text")
+	expectedMsg := "some text"
 	time.Sleep(2 * time.Second)
 	assert.True(t, s.SawOutgoingMessage(expectedMsg))
 	s.Stop()
diff --git a/slash_test.go b/slash_test.go
index e5c79e9..e068fc4 100644
--- a/slash_test.go
+++ b/slash_test.go
@@ -1,6 +1,7 @@
 package slack
 
 import (
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"net/url"
@@ -99,3 +100,70 @@ func TestSlash_ServeHTTP(t *testing.T) {
 		resp.Body.Close()
 	}
 }
+
+func TestSlash_UnmarshalJSON(t *testing.T) {
+	tests := []struct {
+		body                    string
+		wantIsEnterpriseInstall bool
+		wantToken               string
+		wantUnmarshalError      string
+	}{
+		{
+			body:                    `{"token":"blahblah","is_enterprise_install":"false"}`,
+			wantIsEnterpriseInstall: false,
+			wantToken:               "blahblah",
+			wantUnmarshalError:      "",
+		},
+		{
+			body:                    `{"token":"blahblah","is_enterprise_install":false}`,
+			wantIsEnterpriseInstall: false,
+			wantToken:               "blahblah",
+			wantUnmarshalError:      "",
+		},
+		{
+			body:                    `{"token":"blahblah","is_enterprise_install":"true"}`,
+			wantIsEnterpriseInstall: true,
+			wantToken:               "blahblah",
+			wantUnmarshalError:      "",
+		},
+		{
+			body:                    `{"token":"blahblah","is_enterprise_install":true}`,
+			wantIsEnterpriseInstall: true,
+			wantToken:               "blahblah",
+			wantUnmarshalError:      "",
+		},
+		{
+			body:               `{"token":"blahblah","is_enterprise_install":42}`,
+			wantUnmarshalError: "wrong data type for is_enterprise_install: float64",
+		},
+		{
+			body:               `{"token":"blahblah","is_enterprise_install":"unconvertable to bool"}`,
+			wantUnmarshalError: "parsing boolean for is_enterprise_install: strconv.ParseBool: parsing \"unconvertable to bool\": invalid syntax",
+		},
+	}
+
+	for i, test := range tests {
+		var result SlashCommand
+
+		err := json.Unmarshal([]byte(test.body), &result)
+		if err != nil {
+			if err.Error() != test.wantUnmarshalError {
+				t.Errorf("%d: Got error %v, want error %q", i, err, test.wantUnmarshalError)
+			}
+			continue
+		}
+
+		if test.wantUnmarshalError != "" {
+			t.Errorf("%d: Got no error, want error %q", i, test.wantUnmarshalError)
+			continue
+		}
+
+		if result.IsEnterpriseInstall != test.wantIsEnterpriseInstall {
+			t.Errorf("%d: Got IsEnterpriseInstall %v, want IsEnterpriseInstall %v", i, result.IsEnterpriseInstall, test.wantIsEnterpriseInstall)
+		}
+
+		if result.Token != test.wantToken {
+			t.Errorf("%d: Got Token %v, want Token %v", i, result.Token, test.wantToken)
+		}
+	}
+}
diff --git a/tokens_test.go b/tokens_test.go
new file mode 100644
index 0000000..6211745
--- /dev/null
+++ b/tokens_test.go
@@ -0,0 +1,45 @@
+package slack
+
+import (
+	"encoding/json"
+	"net/http"
+	"reflect"
+	"testing"
+)
+
+func TestRotateTokens(t *testing.T) {
+	http.HandleFunc("/tooling.tokens.rotate", handleRotateToken)
+	expected := getTestTokenResponse()
+
+	once.Do(startServer)
+	api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
+
+	tok, err := api.RotateTokens("expired-config", "old-refresh")
+	if err != nil {
+		t.Errorf("Unexpected error: %v", err)
+		return
+	}
+
+	if !reflect.DeepEqual(expected, *tok) {
+		t.Fatal(ErrIncorrectResponse)
+	}
+}
+
+func getTestTokenResponse() TokenResponse {
+	return TokenResponse{
+		Token:         "token",
+		RefreshToken:  "refresh",
+		UserId:        "uid",
+		TeamId:        "tid",
+		IssuedAt:      1,
+		ExpiresAt:     1,
+		SlackResponse: SlackResponse{Ok: true},
+	}
+}
+
+func handleRotateToken(rw http.ResponseWriter, r *http.Request) {
+	rw.Header().Set("Content-Type", "application/json")
+
+	response, _ := json.Marshal(getTestTokenResponse())
+	rw.Write(response)
+}