The code verifier is a cryptographically random string using the characters A-Z, a-z, 0-9, and the punctuation characters -.\_\~ (hyphen, period, underscore, and tilde), between 43 and 128 characters long.
Once the app has generated the code verifier, it uses that to create the code challenge. The code challenge is a BASE64-URL-encoded string of the SHA256 hash of the code verifier.
The app will need to store the `code_verifier` for later use.
Kit appends a `code` query param with a temporary authorization code.
Response schema: *application/json*
This will ensure the user properly navigates back to your app inside of Kit and registers that the app has been installed.
Response schema: *application/json*
Kit appends a `code` query param with a temporary authorization code.
Response schema: *application/json*
This will ensure the user properly navigates back to your app inside of Kit and registers that the app has been installed.
Response schema: *application/json*
`, ``, `` — and mix HTML with markdown. This is useful for finer control, such as nesting a bulleted list inside a numbered list. One caveat: markdown syntax is not processed inside HTML blocks, so use HTML tags for formatting there (e.g. `bold` rather than `**bold**` inside an `- `).
To add extra vertical space between sections, use a paragraph containing only ` `. ### Not supported The following are **not supported** when your description is rendered: * Underline (``) — removed **together with the text inside it**, so avoid it entirely * Images (markdown or ``) — stripped * Horizontal rules (`---` or `
`) — stripped * Manual line breaks (`
` or trailing spaces) — stripped, and the adjacent words are joined with no space. Use blank lines between paragraphs, or a ` ` paragraph for extra spacing * Strikethrough (`~~text~~`) — not parsed; the tildes show up as literal text * `mailto:` and `tel:` links — these render looking like links but do nothing when clicked Tables technically render but with no borders, spacing, or sizing, so we don't recommend using them.The underline behaviour isn't unique to `` — every unsupported HTML tag is removed **together with everything inside it**, so a stray `` or `` will silently delete the text it wraps. Stick to the tags listed in [Using HTML](#using-html). To see exactly how your description will render, click **Preview** at the top of the editor. It opens your app details page rendered with your current (unsaved) changes, using the same markdown pipeline as the live Kit App Store — so what you see in the preview is what creators will see after you save. ## Redirect handling The OAuth redirect parameter automatically sends creators to your app details page after authentication. The URL format is: [https://app.kit.com/apps/\{app\_id}](https://app.kit.com/apps/\{app_id})If you have already published an app, no changes are needed if you're using our dynamic redirect parameter. Your creators will seamlessly move from installation to your getting started guide. Otherwise, you will need to update your authentication flow to end on the redirect parameter appended to the initial call to your configured `authorization URL` ## Previewing your changes While editing, click the **Preview** button (next to **Save** at the top of the App details form) to open a full-screen preview of your app details page. The preview: * Renders your **unsaved changes** — name, summary, icon, images, demo video, categories, resource links, and the markdown description — exactly as the Kit App Store will display them after saving. * Uses the same server-side markdown rendering as the live page, so it's a faithful check of which formatting survives (see [Markdown support](#markdown-support)). * Lets you **save directly from the preview** once you're happy. For published apps, you'll still get the usual confirmation that changes go live for all creators.![]()
## Sharing your app with creators Once you've populated the app details page form, click "Save" to save your changes. You can also preview a saved app from the [Build](https://app.kit.com/apps?is=created) page via its "Preview" option:
## Best practices Your App Details Page is the primary way creators discover, learn about, and evaluate your app. For installation, it serves as both an educational and sales tool for convincing creators to try your app. Below are the components of a good App Details page:
For an example of a high-quality App Details page, [click here](https://app.kit.com/apps/924). # App versioning Source: https://developers.kit.com/kit-app-store/app-versioning Ensure creators have access to your latest functionality through app versioning App versioning in Kit ensures smooth transitions when authentication requirements change, protecting both creators and developers by maintaining compatibility while apps evolve. This guide explains how versioning works, what triggers new versions, and the implications for both creators using your app and developers maintaining it. ## Understanding app versions App versions are automatically created by Kit when certain changes to your app's authentication or scope requirements occur. These versions help manage the authentication lifecycle and ensure creators maintain proper access to your app's functionality. Kit automatically creates new app versions in the following scenarios: * Initial publication (v1) * Authentication changes When installing an app, a creator will always authenticate with the latest available version. ### Initial publication The first version is created when your app is initially published to the Kit App Store. This establishes the baseline authentication and scope requirements for your app going forwards. ### Authentication changes A new version is triggered when your app authentication requirements change after it is released. This ensures that creators are prompted to reauthenticate to gain access to any new features and functionality reliant on the new authentication method or scopes attached. The specific scenarios are: * **Access types change** - When you modify which Kit resources your app can access (API access, plugin access, or both) in your app's *Authentication* settings tab * **Authorization strategy changes** - When your plugin provider's authentication method changes (for example, switching from "No authorization" to "OAuth) in your app's *Authentication* settings tab * **Plugin scopes change** - When the cumulative scope requirements across all your plugins are modified - more details on this can be found below. ## Understanding scope changes Scopes define the specific permissions your plugins require to access data from your service. Kit tracks scopes cumulatively across all plugins in your app, and version changes occur when: * A new plugin is created with a scope that none of your other plugins currently use * A scope is added to an existing plugin that wasn't previously required by any plugin * A scope is removed from a plugin and is not used by any other plugins in your app Scopes allow you to gate access to plugins to only work for creators that have installed the correct version of your app. Highlight your app’s purpose and how it enhances the creator’s experience. Include benefits for both the Kit integration and standalone use Describe how the app works within Kit and what it offers independently as a standalone experience Provide a concise breakdown of key features and their value Best Practice: At least 2-3 high-quality images showcasing app functionality along with an annotation on features or benefits of the App - examples:
![]()
A configuration page lets creators customize how your app integrates with their Kit account—such as choosing which custom fields sync, toggling features on/off, or performing historical imports. This flexibility lets creators tailor the integration to their needs without unnecessary complexity. An example can be found below:
![]()
* A general link to your app/platform support documentation * A Help Center Article specific to your Kit integration with: * Clear setup instructions * FAQs addressing common creator questions Although not required to publish, adding a video to your app details page allows you to market your app to Kit creators even further, showcasing the benefits of your app in a new engaging way For example, if you have two plugins: * Plugin A uses scopes: `read:products`, `read:inventory` * Plugin B uses scopes: `read:products`, `read:customers` Your app's cumulative scopes are: `read:products`, `read:inventory`, `read:customers` Adding `write:orders` to either plugin would trigger a new version, while adding `read:products` to a third plugin would not (since it's already in the cumulative scope set). ### Adding scopes to a plugin Scopes can be added to plugins using the *Scopes* field found when editing a plugin. A plugin can have any number of scopes attached, with previously used scopes available for selection automatically from the field. When creating a new scope, press return upon completion of typing for it to be added to the plugin. Click save to publish this change and prompt upgrade for creators with the app installed.![]()
Using spaces in scope names can cause serialization issues when processed as arrays or comma-separated strings. We recommend using a combination of `action:resource`, such as `read:data` (opposed to `read data`) and hyphens/underscores/snake-case/camel-case for multi-word scopes, such as `read:customer_data`. ## Impact on creators When a new app version is created, the experience varies depending on the type of change: ### Re-authentication requirements Creators will be prompted to re-authenticate your app when: * **New access types are added** - For example, if your app previously only used plugins but now also requires API access * **Authorization strategy changes to OAuth** - When moving from no authentication to OAuth authentication * **New scopes are added** - When your plugins require additional permissions not previously granted When re-authentication is required, creators will be notified of updates required, through: * An icon on the [*Manage tab of the Kit App Store*](https://app.kit.com/apps):* All *Install* buttons will change to *Update* for all app cards across the Kit App Store, as well as an update icon in the top right corner:
* A separate section for apps requiring updates at the top of [*Manage tab of the Kit App Store*](https://app.kit.com/apps?is=installed):
Upon clicking the *Update* button for any apps requiring updates, the creator is guided through the authentication flow to grant all new necessary permissions.
When the update is tied to authentication strategy changes, an additional banner will be shown on the *Authentication* settings page for your app to help you know when an update will be required from creators. ### Seamless updates Some version changes don't require creator action: * **Scope removals** - When permissions are removed, creators maintain their existing authentication * **Access type removals** - When reducing the app's access requirements ## Impact on developers As a developer, understanding app versioning helps you plan updates strategically and minimize disruption for your users. It allows you to gate plugins, to only be accessible when a certain version of your authentication strategy is installed, as well as build your apps iteratively - perhaps launching with API access only, before adding plugins at a later date. Below are a few best practices & common scenarios to ensure you are making the most out of app versioning, to make future app updates as seamless as possible. ### Best practices * **Testing plugins with new scopes before launch** - When plugins are inactive but require new scopes, ensure you re-authenticate the app in the [*Build tab of the Kit App Store\_*](https://app.kit.com/apps?is=created) to test the new functionality before launch. This allows you to verify the authentication flow works as expected before affecting production users. Other creators won't see these changes until you publish the plugin. * **Plan your scopes carefully** - Define comprehensive scopes during initial development to minimize future version changes. * **Group related functionality** - Consider future features when establishing initial scope requirements * **Avoid problematic scope formats** - Never use spaces in scope names and use consistent naming conventions for ease of management (recommended: `action:resource` format) * **Test thoroughly before publishing** - Use test mode to verify all scope changes work correctly. Ensure your OAuth server properly handles the new scope requests and validate that the re-authentication flow provides clear information to creators * **Managing version transitions** - When planning changes that will trigger a new version: * Communicate with your users - If possible, notify creators about upcoming changes through your app's channels * Bundle related changes - Group authentication changes together to minimize the number of versions * Maintain backwards compatibility - Ensure your endpoints can handle both old and new authentication tokens during transition periods * Update your documentation - Keep your app's description and support resources current with the new requirements * **Track your app's version history** - While Kit manages version creation automatically, we recommend maintaining your own changelog documenting what changed in each version. This should include the date and reason for authentication changes & which features correspond to which version requirements ### Common scenarios and solutions **Scenario: Adding a new feature** If you're adding a new plugin that requires additional scopes: 1. Consider whether the feature could work with existing scopes 2. If new scopes are necessary, plan the rollout carefully 3. Test thoroughly in development mode first 4. Communicate the value of the new feature to encourage re-authentication **Scenario: Improving security** When updating from no authentication to OAuth: 1. Implement your OAuth server following Kit's requirements 2. Test the complete flow in development 3. Prepare clear documentation for creators about why authentication is now required 4. Consider providing a grace period where both methods work if technically feasible **Scenario: Reducing permissions** If you're optimizing your app to require fewer scopes: 1. This won't require creator re-authentication 2. Update your code to work with reduced permissions 3. Remove unnecessary scope requests from your OAuth flow 4. This is generally seamless for users but improves security and trust ## Technical considerations ### OAuth server requirements When implementing scope changes, ensure your OAuth server: * Properly validates and returns the requested scopes * Handles incremental authorization if scopes are added over time * Provides clear scope descriptions in the consent screen * Maintains tokens that accurately reflect granted scopes ### Error handling Implement robust error handling for version-related scenarios: * Detect when a creator hasn't granted new required scopes * Provide clear messages about what permissions are needed and why * Guide users to re-authenticate when necessary * Gracefully degrade functionality if optional scopes aren't granted # App Authentication Source: https://developers.kit.com/kit-app-store/authentication Setting up authentication for your app We offer 2 forms of authentication for apps: * OAuth * No authentication Deciding which is right for your app depends on whether you are accessing the Kit API or whether you require authentication to tie a Kit account with an external account for your service. Though we do offer API keys to access the V4 API, these should only be used for testing purposes, with OAuth required for the app to go live.![]()
Though keys offer a quick method of testing the API before finalising your app, some endpoints - such as the `Create purchase` or `Bulk...` endpoints - require OAuth authentication, so will not be able to be tested this way. Specific authentication requirements can be found for each endpoint in the API documentation. The only time OAuth is not required would be for apps that only offer plugin functionality that rely on publically available endpoints that require no authentication. A great example of this would by the Kit GIPHY app, that requires no authorization from the creator for Kit to access GIPHY's library of images. ## Full app authentication flow The authentication flow varies based on whether your app offers API access, plugin access or both. To learn about these flows in depth, visit the [API authentication](/api-reference/authentication) and [plugin authentication](/plugins/oauth-authorization) pages, which will help guide you through the requirements and share examples to help you get up and running. The below diagram and step-by-step outline will describe the full flow when you have both API and plugin access configured for your app. If you are looking to build an app with just API or plugin access, visit the specific [API authentication](/api-reference/authentication) and [plugin authentication](/plugins/oauth-authorization) pages.![]()
## Disconnecting your app The connection between Kit and your service can be torn down from either side. Handling both directions keeps you from holding stale tokens — or Kit holding stale credentials — after a creator walks away. ### When a creator disconnects on your side If a creator disconnects or uninstalls your app from *your* platform, call Kit's OAuth token revocation endpoint to invalidate the credentials Kit issued you. A single call to `POST /v4/oauth/revoke`: * Marks the access token (and any associated refresh token) as revoked. * Marks the Kit-side installation as uninstalled, so your app no longer shows as installed for that creator. * Asynchronously clears any partner credentials Kit is holding for that creator on your behalf. Treat this as your clean teardown signal — one request keeps the connection state in sync on both ends. See the [OAuth token revocation](/api-reference/oauth-token-revocation) reference for the request format and code samples. Authentication begins with the creator installing your app from the [Kit App Store](https://app.kit.com/apps) or your app's details page. They can click the "Install" button on either page. ![]()
![]()
If you want to start this flow from your site as well, utilize our install url, `https://app.kit.com/apps/:app_id/install`, appended with `k_app_id=k_:app_id` (which allows us to attribute sign-ups to your particular app). To find your app id - click the "Preview" button for the app on the [Build tab](https://app.kit.com/apps?is=created) of the Kit App Store and the id will be found in the URL path `app.kit.com/apps/:app_id`. *For example, for the GIPHY app, you would send your users to `https://app.kit.com/apps/717/install?k_app_id=k_717`*
You can also redirect creators back to a specific page on your site after installation completes by adding a `return_to` query parameter. See [externally initiating installations](/kit-app-store/authentication#externally-initiating-installations) for details.
The creator is then sent to your service's OAuth flow, whereby the creator grants Kit access to your platform, in order to retrieve the data needed for your plugin(s). Here, Kit will use the OAuth endpoints served by your authentication server to request access tokens, that will be used to authenticate all future requests to your platform. ![]()
Once plugin access is completed, API authentication begins, with Kit kicking off the flow by making a GET request to the authorization URL you have set up for your app. It is important that at this stage, you store the `redirect` property that is appended to the GET request, as this will be the URL your app will need to redirect to once the Oauth flow is completed Once the creator gives authorization for your service to access the Kit API on your behalf, your app will request an access and refresh token that will be used for all future app calls to the API.![]()
Once API authentication is completed, redirect the user back to the `redirect`, URL provided as a query parameter in the initial authorization request. This will ensure the installation flow is tracked and completed properly. This property currently sends users back to your app's details page, which will help guide them through using and getting the most out of your app they have just added to their creator kit. If you have set up the `Redirect URL after install` field in your app's settings, a modal prompting creators to continue their journey on your configured site will appear at this point. See this section in the [app details page guide](/kit-app-store/app-details-page#how-to-configure) for more details. If the installation was [externally initiated](/kit-app-store/authentication#externally-initiating-installations) with a `return_to` parameter, the creator is redirected directly to that URL instead. ![]()
With installation now complete, both Kit and your service will continue to refresh access tokens as required; using the refresh token shared in the same response as the access token to request an updated access token, when the current one has expired. Per RFC 7009, the endpoint returns `200 OK` even if the token is already revoked, expired, or unknown, so you can safely call it without first checking whether the token is still valid. ### When a creator uninstalls on Kit's side If a creator uninstalls your app from within Kit, Kit will make a POST request to the **revoke token URL** you configured for your plugin, so you can tear down the connection on your end. See [revoking an access token](/plugins/oauth-authorization#revoke-an-access-token) for the request body and code samples. ## Externally initiating installations You can now direct users to install your app directly from your own website or marketing materials, without requiring them to first visit the Kit App Store. This installation flow ensures both plugin and API authentication are completed properly, just like installations initiated from the Kit App Store. After successful installation, users will be redirected back to the Kit App Store where we can track the completed installation. To do this, point users directly to the installation URL using this format: `https://app.kit.com/apps/:app_id/install?k_app_id=k_:app_id` replacing `:app_id` with your specific app ID. The `k_app_id` parameter allows us to attribute new Kit signups to your app. If you'd like the user to return back to your site after completing installation, you'll need to configure allowlisted domains in your [app settings](/kit-app-store/app-details-page#how-to-configure). This is needed for security reasons. Then include a URL, including the scheme, that you'd like the user to be redirected back to when the install is finished in the `return_to` query param. The domain must be one of the domains that you allowlisted in your app settings. `https://app.kit.com/apps/:app_id/install?k_app_id=k_:app_id&return_to=https://yoursite.com/example/path` ### Finding your app ID You can locate your app ID in two ways: ##### From the Build tab * Go to the Build tab in the Kit App Store * Click the 'Edit' button on your app" /> * Extract the ID from the URL (e.g. `https://app.kit.com/apps/924/edit` means your app ID is `924`) ##### From your app details page * The app ID appears at the end of your app's details page URL (e.g. `https://app.kit.com/apps/924`) # Best practices Source: https://developers.kit.com/kit-app-store/best-practices What makes a good App on Kit? Apps in the Kit App Store come in all shapes and sizes, helping our wide range of creators in a multitude of ways, they all however should:## Prohibited apps on Kit The following types of Apps will not be permitted for listing on the Kit App Store: * **Apps that do not use Kit’s APIs, Webhooks, or Plugin Environments:** Apps must meaningfully interact with Kit’s platform; if an app does not make use of Kit’s APIs, webhooks, or plugins in a way that improves a creator’s workflow, it will not be approved. * **Single-Creator Private Apps:** Apps must be designed for broad adoption by multiple creators. Private, single-use apps built for only one specific creator are not eligible for listing in the public Kit App Store. * **Apps that duplicate existing functionality:** If an app replicates the functionality of an existing approved app with no clear differentiation, it will not be listed. (e.g., if a Mighty Networks integration already exists, another App offering the same core features without added value won’t be approved). * **Apps that primarily extract Kit data without providing functionality:** Apps must enhance a Creator’s experience within Kit - not just export data elsewhere. Any app that solely exists to extract user, subscriber, or campaign data from Kit without adding direct functionality or engagement within Kit is prohibited. * **Apps that do not comply with Kit’s data privacy & security standards:** Apps must not share, sell, or misuse creator or subscriber data. Any app that violates Kit’s privacy policy or terms of service will be denied listing. Any app that stores subscriber data externally must have explicit consent mechanisms in place. * **Apps that restrict Kit’s access to user or subscriber data:** Apps should not restrict Kit’s ability to access or process creator account info, email lists, or campaign data. * **Apps that automate sending unsolicited or non-compliant emails:** Any app that automates sending unsolicited bulk emails or enables spam-like behavior will be rejected. Apps must comply with all email regulations (CAN-SPAM, GDPR, etc.) and cannot facilitate non-consensual communication. * **Apps that misrepresent functionality or use deceptive marketing:** Any app that misleads creators about its functionality or falsely claims integrations that do not exist will not be approved. * **Apps that have no support resources:** Apps must provide clear support documentation or a designated support contact. Every app should have a help center article specific to its Kit integration, beyond just generic product documentation. Apps that lack any visible support structure will not be approved. # Building apps Source: https://developers.kit.com/kit-app-store/building-apps Building an app for the Kit App Store Kit is opening up a world of opportunities and functionality for our creators by empowering third-party developers like you to build on top of our application and better integrate the creator’s toolkit in one place. Building an app on Kit is completely self-serve. Here's how you can build, test, and publish an app in your Kit account. ## Creating your app Visit the Kit App Store by going to **"Automate"** in the top navigation menu, followed by [Apps](https://app.kit.com/apps). From there, select the [Build tab](https://app.kit.com/apps?is=created), and click + New app at the top right. * connecting external tools or platforms to Kit * enhancing Kit’s functionality through plugins such as our email editor content blocks * or, streamlining workflows or boosting audience monetization/engagement A form will pop up. Fill it out with your app's name, and click "Save". Once you do this, you'll be directed to an **app details** settings page for your newly created app.
The details you provide here will be shown on the app details page that appears when users click your app's "Learn more" option in the Kit App Store.
![]()
Preview the app details page by clicking your app's "Preview" option in the ["Build" tab](https://app.kit.com/apps?is=created) of the Kit App Store. Only the `App name` field is required when you create and test your app. But you'll need to provide more information before you can publish the app to the Kit App Store. This information includes: * Icon * Summary * Description * Resource links Click "Save" to save the changes to your app's details. ## API and plugin access An app can contain either API access, one or many plugins, or both. Here are the key differences: * **API access:** You can allow creators to install apps that can link together external platforms to work in harmony. We control authentication here and developers have to request access on behalf of a creator to us to approve. This is all built upon V4 of our API. * An example API only app would be TeachKit - that solely utilizes the Kit APIs to manage subscribers that use TeachKit's free online courses. * **Plugins:** You can add content directly into the Kit app UI. Kit needs to authenticate against the third party to be able to pull the data required to be rendered within Kit. * An example plugin only app would be GIPHY - which utlises the [media source plugin](/plugins/media-source/overview) to allow creators to insert gifs directly from GIPHY into their email content. No additional API access is required for this app. * **Both:** You can also have apps that combine API and plugin access, with apps such as Mighty Networks offering subscriber and tag syncing alongside [content block plugins](/plugins/content-blocks/overview), helping creators manage and promote their communities through Kit. To set up your app authentication strategies, visit the "Authentication" tab, where you can configure and toggle API and plugin access on and off.For more details on authentication, visit our [app authentication guide](/kit-app-store/authentication), or visit the API and plugin specific pages: * [API](/api-reference/authentication) * [Plugins](/plugins/oauth-authorization) ## Testing your app View your apps on the ["Build" tab](https://app.kit.com/apps?is=created) of the Kit App Store. You'll be able to see: * Whether you have installed the app—from the display of the green installed tick, or * Whether it is a draft, from the "Draft" badge in the bottom corner. When is the "Draft" status, only you are able to see the app in your own account.
From here, click Preview to view your app's app details page, as well as install it within your own account.
![]()
The app will not be publically discoverable for other Kit users to install until you've published it. ### V4 API Keys Though all apps accessing our API require OAuth authentication before they can be published, a great way of prototyping and testing our API's functionality is to use V4 API keys. More details on creating and managing API keys [can be found here](/api-reference/authentication).API keys are meant for individual use only. If you're creating something for people to use, you'll need to build an app—giving you access to plugins, bulk and purchase endpoints, higher rate limits, and more. # Going live Source: https://developers.kit.com/kit-app-store/going-live Getting your app live onto the Kit App Store Once you have built and tested out your app, it's ready to be published to the Kit App Store. The guide below helps map out how to get yoru app live and what to check before submitting for approval. ## App review checklist To streamline the review process and reduce delays, apps must submit the following: ### ✅ App requirements * App authentication * Developers must use OAuth for user authentication instead of API keys for a secure, seamless installation experience when Kit API access is required * In order to allow tracking and validation of installs, the installation process must either: * start and end on the Kit App Store * start the flow externally, utilizing the correct installation URL, with the appended attribution tracking: `https://app.kit.com/apps/:app_id/install?k_app_id=k_:app_id`. Details on setting this up [can be found here](/kit-app-store/authentication#externally-initiating-installations) * you can also utilize the `Redirect URL after install` functionality ([which can be found here](/kit-app-store/app-details-page#how-to-configure)) to send creators to an external site upon completion of the install flow and redirect back to Kit. * General UX * Apps should offer intuitive navigation and an easy onboarding experience * Clear access to help center articles or other support documentation is mandatory to minimize user confusion * Technical standards * Apps must follow [standard best practices](/api-reference/response-codes#429-%7C-rate-limiting) to avoid API rate limiting * Apps should follow the relevant plugin recommendations * App details page * Ensure your app details page follow the best practices set out in [the app details guide](/kit-app-store/app-details-page#best-practices), to ensure Kit App Store quality, but also help drive installations for your app * Visual Automation templates * If you ship [VA templates](/kit-app-store/va-templates) with your app, each template goes through Kit review. If your app is **already published**, new templates are reviewed on their own. If your app is **not yet published**, templates are reviewed **together with the app** when you submit the app for approval—follow the in-product banners and the [VA templates guide](/kit-app-store/va-templates#creating-and-submitting-a-template) so reviewers can evaluate both in one pass. ### ✅ Functionality description * Submit a clear, concise description of the app’s intended functionality so that we know what to test it for * Explain the app’s key flows and use cases (both within and outside Kit as a standalone experience) so testers understand what to evaluate ### ✅ Test credentials & OAuth testing If the app requires a paid account, developers must provide us with test credentials for use during the review process. Apps must support all potential OAuth flows. Testers will evaluate: * **Not logged in:** Testing OAuth from a logged-out state * **Logged in:** Testing OAuth from an already logged-in account * **New User Signup:** Supporting a net new account creation (you’ll need to provide us with the ability to create a trial account or use a promo code to enable this) * **Pre-loaded Data:** For apps with sync functionality (e.g., importing contacts), the test account must include pre-loaded data to simulate a realistic creator experience ### ✅ OAuth & onboarding * Developers must use OAuth for user authentication instead of API keys for a secure, seamless installation experience * The installation process must start and end on Kit in order to allow tracking and validation of installs ## Publishing your app Once you've gone through and adhered to the points outlined above, send the app for approval by clicking the "Submit for approval" button in the "Distribution" tab within your app settings:Click the "Submit for Approval" button in the window that pops up to confirm.
Your app will be submitted to us for approval. If we need a test account with your service to review your app, please send the test account's information to [apps@kit.com](mailto:apps@kit.com) alongside any details on the app functionality and steps for testing. Details on what makes a great app and what to avoid can be found in our [best practices guide here](/kit-app-store/best-practices). Once approved by our team, the developer account will receive an email that the app is ready to be published. When ready, hit "Publish" in the "Distribution" tab of your app and your app will automatically be available in the [Kit App Store](https://app.kit.com/apps) for all eligible creators (currently all paid plans), as well as our [Kit app marketing site](https://kit.com/apps). If we reject your app, we'll send you an email explaining the issues we found. You can then make changes to your app and click "Resubmit for approval" to have us review it again.
# Kit Developer Assistant Source: https://developers.kit.com/kit-app-store/kit-developer-assistant The [Kit Developer Assistant](https://chatgpt.com/g/g-688120c86d6c819190cd42ec2f26fcd3-kit-developer-assistant-kit-app-store) is your on-call technical guide for building on the Kit platform. It’s a specialized version of ChatGPT trained on Kit’s developer documentation, App Store guidelines, API reference, and development best practices. **What you can use the Kit Developer Assistant for:** * Understanding Kit’s API endpoints, authentication flows, and plugin architecture in plain language * Providing code samples and integration patterns tailored to your app type (Media Source, Subscriber Sync, Email Editor Plugins, etc.). * Highlighting required fields and submission criteria for App Store listings * Walking you through common tasks like OAuth setup, post-install redirects, and handling webhooks * Troubleshooting integration issues by spotting gaps or missteps in your flow * Drafting your App Details page copy in seconds **Tip:** The Kit Developer Assistant is a complement to, not a replacement for, the official documentation - use both together for the best results. If you notice any inconsistencies from the Kit Developer Assistant, please email us your feedback at [apps@kit.com](mailto:apps@kit.com). # Managing your apps Source: https://developers.kit.com/kit-app-store/managing-your-apps Updating your app once it's live Once an app has been published, it will be available for all creators to install and use - but what happens if you want to edit your app, stop new users from installing it or removing it completely from the Kit App Store? ## Editing your app Once an app is live, you can find and edit it from within the ["Build" tab](https://app.kit.com/apps?is=created) of the Kit App Store. Here you can update your app details page, your authentication settings or create and manage existing plugins. If your app offers **Visual Automation templates**, you can create, submit, unpublish, and delete them from your app’s **Templates** tab. How review, publish, and locked-automation behavior work is documented in the [Visual Automation templates guide](/kit-app-store/va-templates).
![]()
It is important to note that once published, any updates to your app will be reflected immediately, for all creators that have already installed your app as well as new installations. Therefore, we recommend that you make edits on an unpublished test version of the app, so that you can test and preview the changes thoroughly, before moving them across to the published app. ## Unpublishing or deleting your app To **unpublish your app**, go to the [Build tab](https://app.kit.com/apps?is=created) and click your app's "Edit" button. Navigate to the "Distribution" tab, and click the "Unpublish" button at the bottom to unpublish it.When you unpublish your app, it will no longer be publicly visible in the Kit App Store. However, **creators who already have your app installed can continue to use it**. To make your unpublished app unavailable for use (while still keeping it installed for creators), you'll need to: * Deactivate the app's plugin authentication, and/or * Pause the sending of API calls from your app. Alternatively, you can **delete your app**. This will remove it from the Kit App Store and the accounts of creators who have installed it. These creators will no longer be able to use your app. Delete your app by clicking your app's "Edit" button from the [Build tab](https://app.kit.com/apps?is=created) in the Kit App Store. Navigate to the Distribution tab, and click the "Delete" button at the bottom.
# Kit App Store overview Source: https://developers.kit.com/kit-app-store/overview **Welcome to the Kit App Store!** Kit is the email-first operating system for creators who mean business. Our platform serves serious creators who have transformed their expertise into successful full-time businesses using Kit to grow their email lists, manage subscribers, and automate their marketing. The Kit App Store helps creators run their businesses more efficiently by filling gaps between the multiple services they rely on. By building apps for Kit, you'll serve thousands of creators who collectively reach millions of subscribers worldwide. ## Why build a Kit app? Kit is home to a thriving ecosystem of serious creators who have transformed their expertise into successful full-time businesses. These creators don't just dabble in content creation—they foster deep relationships with their audience to build profitable and sustainable businesses that add real value to the people they serve. When you develop an app with Kit, you’re tapping into: * \$250 billion creator economy market * 60,000+ Kit creators * 2.1 billion emails sent on average per month * \$1.2 millions sales on average per month 70% of Kit customers have at least one app or legacy integration installed. With in-app and Kit newsletter spotlights, your app will reach our customer base and help them solve every-day creator pain points. ## Different types of apps you can build The Kit App Store supports a diverse range of applications designed to enhance creators' workflows and business operations. * **Customer relationship management:** Connect to Kit’s subscriber tag management system and build apps that help creators track and follow up with prospects efficiently for sales deals, podcast scheduling, affiliate deals, and more. * **Content distribution:** Connect to Kit’s email editor or media gallery and automatically pull in content from other tools into emails more efficiently to drive more email engagement, more traffic to your content, and more revenue—all while saving time. * **Digital products & memberships:** Build apps that help creators set up and sell digital products, courses, or membership groups that sync with a creator’s email list in Kit. Make it easy for creators to build, sell, and manage their customers all in one place. * **AI-assistance:** Explore AI opportunities that help creators build content outlines, write compelling subject lines, promote products, write newsletters, and more * **Data analysis:** Use Kit’s API endpoints to pull email performance data and build dashboards or reports to help creators better understand what’s working and where to improve to drive better results. * **Audience learning & building:** Creators are always looking for ways to gain more information about their subscribers for better personalization and segmentation. From surveys to quizzes, build an app that gives creators the opportunity to learn even more about their subscribers. * **Task and workflow management:** Create productivity apps that help creators manage their content calendars, automate repetitive tasks, and streamline their business operations within the Kit ecosystem. Apps can be built using either API access to connect external platforms or as plugins that add content directly into Kit's UI. The self-serve development platform gives you complete control over your app's functionality and user experience. Here’s the [quick start guide](/kit-app-store/quick-start-guide) to start building your app. # Quick start guide Source: https://developers.kit.com/kit-app-store/quick-start-guide Getting you up and running on the Kit App Store ## Getting started on the Kit App store Welcome to the Kit App Store! Whether you’re building an app to help creators streamline their workflows, enhance monetization, or improve audience engagement, this guide will walk you through the key steps to getting started. Before you begin building, follow these steps to set yourself up for success: * **Create a Kit account:** To get API Access, you first need to create a Kit account. Then, go to [the "Build" tab](https://app.kit.com/apps?is=created) on the Kit App Store to create your app. * **Join our developer community:** Request access to our Kit developer community by emailing [apps@kit.com](mailto:apps@kit.com). This is the best place to ask questions, get support from peers, and connect with other developers. * **Review our technical documentation:** Familiarize yourself with [Kit’s APIs](/api-reference/overview) and [plugin environments](/plugins/overview) by checking out the Kit developer docs. * **Understand app requirements & guidelines:** Make sure your app aligns with our [app requirements checklist](/kit-app-store/going-live#app-review-checklist). Apps that do not meet our standards will not be approved for listing on the Kit App Store. ## Building your App on Kit Once you’re set up, it’s time to start building! * **Use OAuth for authentication:** Kit supports OAuth-based authentication for secure user sign-ins. Learn how to implement your [app authentication here](/kit-app-store/authentication). * **Design for a seamless user experience:** Your app should provide a clear and intuitive user experience. Follow our [best practices](/kit-app-store/best-practices) to ensure a smooth experience. * **Ensure proper API usage:** Your app must make meaningful use of Kit’s APIs, webhooks, or plugins to be approved. * **Prepare your app listing:** Every app needs a high-quality app details page that helps creators understand what it does. Think of this as both a way to guide creators as well as pitch them on installing your app! Follow our [guide for best practices](/kit-app-store/app-details-page) for filling this out. For a more detailed guide on building apps, see our [full guide for building apps here](/kit-app-store/building-apps). Runnable examples often beat prose. Our examples repository holds working OAuth, content block, and media source apps you can clone and run:
## Submitting your app for review When your app is ready, follow these steps to submit it for review: * **Complete the pre-submission checklist:** Ensure your app meets all technical and UX requirements. See the [full checklist here](/kit-app-store/going-live#app-review-checklist). * **Submit your app:** Submit your app for approval via the "Distribution" tab while editing your app, and provide app details, test credentials (if applicable), and a brief description of functionality to [apps@kit.com](mailto:apps@kit.com). * **Review & approval timeline:** The Kit team will review your app within 5 business days and provide feedback via email. If changes are required, you will receive an email that your app has been rejected, but don’t fret! That just means you need to update and resubmit based on the required changes. * **Go live & start driving installs:** Once approved, publish your app from the "Distribution" tab and your app will be listed on the Kit App Store, as well as other marketing placements on Kit’s website, and you can start promoting it! ## Ongoing support & resources * **Developer docs:** Find troubleshooting guides, FAQs, and more throughout our developer docs. * **Get help:** If you run into issues, reach out via [apps@kit.com](mailto:apps@kit.com), [reach out to our support team](https://kit.com/support), or post in our developer Slack community (please note that the Slack community is not an expedited support channel and is more for peer-to-peer help from other developers who have built Kit apps). # Visual Automation templates Source: https://developers.kit.com/kit-app-store/va-templates Ship pre-built Visual Automations with your Kit app so creators can adopt working workflows in seconds. Visual Automation (VA) templates let you attach **pre-built automations** to your app. When a creator installs your app, they can discover those templates, preview them, and copy them into their own account—so they get value from your integration faster than building flows from scratch. Templates are **bound to your app**: Kit only surfaces them to creators in the right contexts (for example, when your app is installed or on your [app details page](/kit-app-store/app-details-page)). Publishing and review follow Kit’s app quality bar; see [Going live](/kit-app-store/going-live) and the sections below. VA templates are managed in the Kit product UI (Build → your app → **Templates**). There are no public API endpoints for creating or publishing app VA templates today. If programmatic access ships later, it will appear in the [API reference](/api-reference/overview). ## Prerequisites and eligibility * **Developer account and app:** You manage templates from the same [Build](https://app.kit.com/apps?is=created) area where you edit your app. You need a Visual Automation in your developer account to use as the source for a template. * **Visual Automations and paid access:** Visual Automations are a **paid** feature in Kit, and authoring VA templates requires that access. If you do not have access but want to add automation templates to your **public** app, email [apps@kit.com](mailto:apps@kit.com); the team will try to help (for example with trial access so you can build and test templates before you ship). * **Published app for creator discovery:** App-bound templates are shown to creators in contexts that require your app to be **published** in the Kit App Store. Keep your [app details page](/kit-app-store/app-details-page) and support links accurate so creators know how to use adopted automations. * **Creator visibility:** Templates are only relevant when the creator has (or can install) your app. Creators who have not installed your app may still see template previews on your app details page in some flows, with install or upgrade paths as appropriate. ## Where to manage templates 1. Open Kit and go to **Automate → Apps → Build** ([direct link](https://app.kit.com/apps?is=created)). 2. Click **Edit** on your app. 3. Open the **Templates** tab. Here you can create templates, submit them for review, unpublish or delete them, and open previews. Build and edit the Visual Automations you use as sources in the same account at **[https://app.kit.com/automations/visual](https://app.kit.com/automations/visual)**. ## Creating and submitting a template## Review and publish states * **In review:** The template is not yet available to creators. If your app is unpublished, align with **Submit app for review** (and any email instructions from Kit) so the app and template can be evaluated together. * **Approved:** After approval, the template is **published automatically**—you do not need a separate publish step for the initial go-live. * **Rejected or changes needed:** Use the feedback from Kit to update your app or template and resubmit following the same patterns as [app resubmission](/kit-app-store/going-live). For overall timelines and how to contact the review team, see [Going live](/kit-app-store/going-live#publishing-your-app) (including [apps@kit.com](mailto:apps@kit.com) for test credentials and testing notes). ## After publish: unpublish, delete, edits, and re-review ### Unpublish You can **unpublish** an approved template from the templates list or template detail view. Unpublished templates are no longer offered for new adoptions; use this when you need to pause discovery while you fix copy or product issues. ### Delete **Delete** removes the template from your app’s templates area. Kit asks for confirmation first. **Creators who already adopted the template keep their copy**—deletion does not remove automations from their accounts. ### Editing a published template * You **cannot edit the automation graph** while it is bound as a published template. The Visual Automation editor shows a **locked**, read-only view with a banner explaining that the automation is published as a template and that changes are managed from **app template settings**. The banner includes a link to the **Help Center** for creators and developers who need more context on locked automations. * You **can change which Visual Automation** backs an existing template when Kit allows that action in the template settings UI. * **Substantive changes** (anything that requires Kit to re-evaluate the template) trigger **re-review**. The template detail view shows a notice that changes will be submitted for review again—submit when you are ready. In **Visual Automations**, build and test the automation you want to ship as a template. Use nodes, [content blocks](/plugins/content-blocks/overview), and especially [dynamic content](/plugins/content-blocks/dynamic-blocks/overview) patterns that will still make sense after a creator copies the flow into their account (see [Building automations that template well](#building-automations-that-template-well)). Start the create flow, choose the Visual Automation that backs the template, and give the template a **display name**. The name is **separate from** the underlying automation’s name: changing one does not automatically rename the other. Kit may pre-fill the template name from the automation name; you can edit it before submitting. Kit shows different guidance depending on whether your app is **already published** on the Kit App Store: * **App already published:** Your submission is a **template-only** review. The banner explains that the template will go through review and typical timing expectations. * **App not yet published:** The template is reviewed **together with the app** when you submit the app for approval. Follow the CTAs to submit your app if you have not yet. Submit the template (and app, if applicable) so Kit can review it. You will see **in review** states on the template list and detail views until a decision is recorded. Do not rely on editing the underlying graph in place for a live template. Use template settings (or unpublish, then iterate in a separate automation) so creator expectations and review state stay consistent. ## Preview From template management, use **Preview** to open how the template appears to **creators** in a **new browser tab**. Use this to verify copy, entry points, and node configuration before and after review. ## Creator-facing behavior Understanding what creators see helps you write [app details](/kit-app-store/app-details-page) copy, [help article URLs](/kit-app-store/app-details-page#how-to-configure), and support docs. * **App details page:** Published apps can show one or more template cards with actions such as preview, install, or adopt, depending on the creator’s plan and whether they already installed your app. * **Visual Automation template library:** App templates can appear alongside other templates, with ordering and eligibility rules that favor apps the creator uses. * **After install:** Creators may see prompts that highlight your templates the first time they open relevant surfaces after installing your app. For end-user concepts (how creators use Visual Automations in general), Kit’s help centre includes an overview: [How to use Kit Visual Automations](https://help.kit.com/en/articles/2502666-how-to-use-kit-visual-automations). ## Building automations that template well * **Dynamic content:** If your template uses app-driven or per-subscriber data in emails, follow the [Dynamic content blocks](/plugins/content-blocks/dynamic-blocks/overview) guides (configuration, Liquid, and plugin flow) so adoption duplicates the automation cleanly for creators. * **Other content blocks:** Prefer patterns documented under [Content blocks](/plugins/content-blocks/overview) and related guides so duplicated automations keep working when creators adopt your template. Avoid hard-coding developer-account identifiers where a creator’s resources should be used instead. * **Entry triggers and goals:** Make the default path obvious after adoption (tags, segments, and goals should make sense on a fresh creator account). * **OAuth and settings:** If the automation assumes your app is installed, state that in your app description and help article so creators connect your app before turning on the flow. * **Node notes for adopters:** In the Visual Automation editor, add **notes** on nodes (where Kit supports them) to explain non-obvious steps, what the creator should configure after copying, and how your app fits in. Those notes help creators successfully run the template after they adopt it. ## Support and review contact * **Review questions and test credentials:** [apps@kit.com](mailto:apps@kit.com) — same channel as [app review](/kit-app-store/going-live). * **Ongoing template lifecycle:** After your app is live, you can still add, update, and remove templates from the **Templates** tab; see also [Managing your apps](/kit-app-store/managing-your-apps). ## Limitations * **API:** Template create/update/publish is not available on the public Kit API today. * **Multiple templates:** Kit may expose more templates per app over time; start from what you see in the **Templates** tab for your app. * **Feature availability:** If a capability is not visible in your account, your plan or rollout group may not include it yet—use Preview on your [app details page](/kit-app-store/app-details-page) and in-app messaging for the source of truth. If you think you should have access (for example to build templates for a public app), contact [apps@kit.com](mailto:apps@kit.com). # Kit Developer Docs MCP Source: https://developers.kit.com/mcp/kit-developer-docs-mcp Connect AI coding agents directly to Kit's developer documentation using the Model Context Protocol The Kit Developer Docs MCP (Model Context Protocol) server gives AI coding agents direct, real-time access to Kit's developer documentation. Rather than copying and pasting docs into your AI client, your agent can query Kit's full API reference, App Store guidelines, plugin architecture, and more on demand — always up to date. In supported clients, your agent can go further and make live Kit API calls on your behalf and spin up local servers to test OAuth flows end-to-end. ``` https://developers.kit.com/mcp ``` Add this URL as a custom connector in your MCP-compatible AI client — for example, by [adding a custom connector in Claude](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp). See full setup guides below. Looking for the [Kit MCP](/mcp/kit-mcp) instead? It lets AI clients act on a creator's Kit account, while this MCP gives your agent live access to Kit's developer docs. See the [MCP overview](/mcp/overview) for how the two compare. Using the MCP server is also significantly more effective than relying on your AI client's web search: * **Always current** — MCP queries the live documentation directly, so your agent is never working from a stale search index or cached page * **Token efficient** — web search retrieves full pages including navigation, markup, and boilerplate, all of which consume context window tokens. MCP returns only the structured content your agent actually needs, keeping responses faster and context usage lean ## What your agent can do Once connected, your AI agent can: * Query any Kit API v4 endpoint reference, including request parameters, response shapes, and authentication requirements * Understand Kit App Store requirements, submission criteria, and app configuration options * Look up plugin component library usage, content block flows, and automation node configuration * Reference OAuth flows, webhook event schemas, and pagination patterns * **Make live API calls on your behalf** — in supported agentic clients (such as Claude Desktop, Claude Code, and Cline), your agent can use the API reference to construct and execute Kit API requests directly, without you writing a single line of code * **Spin up a local OAuth server for testing** — ask your agent to start a local redirect server so you can complete the OAuth authorization flow end-to-end in your development environment before shipping. See [App Authentication](/kit-app-store/authentication) for how to configure OAuth in your app and make authenticated API calls ## Setup guides**Upgrading from the previous setup?** Earlier versions of this guide used `kit` as the config key. If you have an existing entry, rename it to `kit-docs` (or remove the old one) to match the new layout and avoid running two duplicate Kit servers in the same client. ### Claude Desktop 1. Open Claude Desktop and go to **Settings → Developer → Edit Config**, or open the config file directly: * macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` * Windows: `%APPDATA%\Claude\claude_desktop_config.json` 2. Add the Kit Developer Docs MCP server: ```json theme={null} { "mcpServers": { "kit-docs": { "url": "https://developers.kit.com/mcp" } } } ``` 3. Save and restart Claude Desktop. The Kit docs will be available as a tool in all your conversations. ### Claude Code Run the following command in your terminal: ```bash theme={null} claude mcp add --transport http kit-docs https://developers.kit.com/mcp ``` The Kit Developer Docs MCP server will be available in all Claude Code sessions in that project. ### Cursor 1. Open Cursor and go to **Settings → Tools & Integrations → MCP Tools → New MCP Server**. 2. Add a new server with the following configuration: ```json theme={null} { "mcpServers": { "kit-docs": { "url": "https://developers.kit.com/mcp" } } } ``` Alternatively, create or edit `.cursor/mcp.json` at the root of your project with the same configuration. See [Cursor's MCP guide](https://docs.cursor.com/context/model-context-protocol) for more details. ### Windsurf 1. Open Windsurf and go to **Windsurf Settings → Cascade → MCP Servers → Add Server**. 2. Select **HTTP/SSE** as the server type and enter `https://developers.kit.com/mcp` as the URL. Or edit `~/.codeium/windsurf/mcp_config.json` directly: ```json theme={null} { "mcpServers": { "kit-docs": { "serverUrl": "https://developers.kit.com/mcp" } } } ``` See [Windsurf's MCP documentation](https://docs.windsurf.com/windsurf/mcp) for more details. ### Cline (VS Code) 1. Open VS Code with the Cline extension installed. 2. In the Cline sidebar, click the **MCP Servers** icon and then **Edit MCP Settings**. 3. Add the Kit server: ```json theme={null} { "mcpServers": { "kit-docs": { "url": "https://developers.kit.com/mcp", "transport": "http" } } } ``` See [Cline's MCP documentation](https://docs.cline.bot/mcp/connecting-to-a-remote-server) for more details. ## Other ways to use AI with Kit docs The MCP server is the recommended way to give your AI agent access to Kit's developer documentation — it's real-time, always up to date, and requires no manual steps. For AI clients that don't yet support MCP, there are two additional options. ### llms.txt [llms.txt](https://developers.kit.com/llms.txt) is a single file containing all the content in the Kit developer documentation hub, following an emerging industry standard for making web content accessible to LLMs. You can load it by pasting the URL directly into your AI client, or copying and uploading the file if your client doesn't support URL reading: * [**Open in Claude**](https://claude.ai/new?q=Read+from%20https%3A%2F%2Fdevelopers.kit.com%2Fllms.txt) * [**Open in ChatGPT**](https://chatgpt.com/?hints=search\&q=Read%20from%20https%3A%2F%2Fdevelopers.kit.com%2Fllms.txt%20so%20I%20can%20ask%20questions%20about%20it.) * **Cursor:** Add `https://developers.kit.com/llms.txt` as a docs source via [Cursor's @Docs feature](https://docs.cursor.com/context/@-symbols/@-docs) * **Other LLMs:** Request your client to read from `https://developers.kit.com/llms.txt`llms.txt can become large enough to exceed some LLMs' context windows. If that happens, use the page-level method below, or switch to the MCP server which retrieves only what's needed on demand. ### Page-level For focused questions on a single topic, every page in the docs has a **Copy page** button in the top-right corner. This copies the page's markdown content, which you can paste directly into any AI client — or use the **Open in ChatGPT / Open in Claude** shortcuts to start a conversation immediately.## Tips for working with AI agents * **Be specific in your prompts.** Ask your agent to "look up the Kit API v4 subscribers endpoint" rather than "how does Kit work" — targeted queries return better results. * **Combine with page-level context.** For deep dives into a single topic, use the "Copy page" button on any docs page alongside your MCP-connected agent. * **Verify important details.** AI agents can misinterpret or hallucinate details — always verify generated code against the [API reference](/api-reference/overview) before shipping.
If you run into issues or have feedback on the Kit Developer Docs MCP server, reach out to us via the [Kit Developer Community](https://kit.typeform.com/to/f8urvmPe). # Kit MCP Source: https://developers.kit.com/mcp/kit-mcp Let AI clients act on a creator's Kit account via the Model Context Protocol. The Kit MCP is a remote MCP server that exposes a creator's Kit account — subscribers, sequences, broadcasts, tags, segments, and more — to MCP-compatible AI clients. Where the [Kit Developer Docs MCP](/mcp/kit-developer-docs-mcp) gives your agent read-only access to these docs, the Kit MCP gives an AI client the ability to read and write live account data on the creator's behalf. ``` https://app.kit.com/mcp ``` Add this URL as a custom connector in your MCP-compatible AI client. Step-by-step instructions for each major client live in the [help centre guide](https://help.kit.com/en/articles/14827557-how-to-connect-the-kit-mcp-to-your-ai-tools).**Canonical guide.** The complete setup and usage documentation for the Kit MCP lives in the Kit help centre: [Using the Kit MCP](https://help.kit.com/en/articles/14827557-how-to-connect-the-kit-mcp-to-your-ai-tools). This page is a developer-oriented brief — for step-by-step setup, head to the help centre. ## Who it's for * **Creators** using AI clients (Claude Desktop, ChatGPT, Cursor, and others) to manage their Kit account through natural language. * **Developers** building tools, scripts, or workflows on top of an authenticated creator session — for example, internal automations or AI-driven assistants that act against a single account. * **Kit App Store builders** who want to dogfood agentic flows during development, or prototype AI features against a real account before formalising them as an app. ## Availability The Kit MCP is available on all paid Kit plans (Creator and Creator Pro). ## What it can do The Kit MCP is essentially a one-to-one mapping of Kit's v4 API surface — every endpoint in the [API reference](/api-reference/overview) is exposed as an MCP tool. That covers, for example: * **Read account data** — subscribers, sequences, broadcasts, tags, segments, custom fields, and account-level info. * **Manage subscribers** — create, update, tag, untag, and segment. * **Send and draft broadcasts** — compose, schedule, and send to filtered audiences. For the full tool catalogue, see the [help centre guide](https://help.kit.com/en/articles/14827557-how-to-connect-the-kit-mcp-to-your-ai-tools). ## Safety Every tool the Kit MCP exposes carries an annotation — `readOnlyHint`, `destructiveHint`, `idempotentHint`, or `openWorldHint` — that supported AI clients use to decide when to ask for confirmation. Read operations run without prompts; write operations apply appropriate safety layers; destructive or external actions (sending broadcasts, deleting data) always require explicit user confirmation. ## Authentication The Kit MCP authenticates via OAuth on behalf of the creator. On first connect, your AI client opens a Kit authorization flow — the creator signs in and grants access — and the resulting access token is scoped to that creator's account. Tokens use the same OAuth model as Kit App Store apps; see [Authentication](/api-reference/authentication) for the underlying flow, and the [help centre guide](https://help.kit.com/en/articles/14827557-how-to-connect-the-kit-mcp-to-your-ai-tools) for client-side setup. Requests are rate-limited to 120 per minute per token. ## How it relates to other Kit developer surfaces * **[Kit API v4](/api-reference/overview)** — programmatic, code-driven access. Use this when you're building an app, script, or integration. The Kit MCP sits on top of the API as an interface layer for less technical creators, letting them work with the same capabilities through an AI client without writing API calls directly. * **[Kit Developer Docs MCP](/mcp/kit-developer-docs-mcp)** — read-only access to these developer docs for your AI coding agent. No account access.If you run into issues or have feedback on the Kit MCP from a developer perspective, reach out via the [Kit Developer Community](https://kit.typeform.com/to/f8urvmPe). For creator-side support, the [help centre guide](https://help.kit.com/en/articles/14827557-how-to-connect-the-kit-mcp-to-your-ai-tools) is the place to start. # MCP overview Source: https://developers.kit.com/mcp/overview How to access Kit programmatically — the REST API and Kit's two MCP servers. Kit gives you three ways to work with the platform programmatically: the REST API for building integrations against Kit data, the Kit Developer Docs MCP for giving your AI coding agent live access to these docs, and the Kit MCP for letting an AI client read and write to a creator's Kit account on their behalf. ## Choose your starting point## When to use which * **Building a Kit App Store app or a custom integration** — use the [Kit API v4](/api-reference/overview), and connect the [Kit Developer Docs MCP](/mcp/kit-developer-docs-mcp) so your AI coding agent always has the latest endpoint reference, OAuth flows, and App Store guidelines. * **Automating your own (or a creator's) Kit account through an AI client** — use the [Kit MCP](/mcp/kit-mcp). Natural-language prompts like "draft a broadcast to my new subscribers from this week" become real account actions. * **Your AI assistant keeps writing stale Kit code** — connect the [Kit Developer Docs MCP](/mcp/kit-developer-docs-mcp). It queries the live docs so your agent stops working from a cached or hallucinated API surface. ## Authentication at a glance * **Kit API v4** — API keys for personal automations, OAuth for apps acting on behalf of a creator. See [Authentication](/api-reference/authentication). * **Kit Developer Docs MCP** — no auth required. The docs are public. * **Kit MCP** — OAuth on behalf of the creator. The AI client opens a Kit authorization flow on first connect, and tokens are scoped to that creator's account. See the canonical [help centre guide](https://help.kit.com/en/articles/14827557-how-to-connect-the-kit-mcp-to-your-ai-tools) for client-side setup. Build apps and integrations against Kit's REST API. Give your AI coding agent real-time access to these developer docs. Let an AI client read and write to a creator's Kit account on their behalf. The Kit MCP's complete setup and usage guide lives in the Kit help centre — see [Kit MCP](/mcp/kit-mcp) for the developer brief and the link through. # Color picker Source: https://developers.kit.com/plugins/component-library/color-picker The color picker allows creators to customize content to better reflect their branding and style. It is commonly used to allow the creator to change the color of backgrounds, buttons and text to better fit in with their email templates.## Compatibility | Plugin type | Availability | Additional notes | | -------------- | ---------------------------- | ---------------- | | Content blocks |
| | | Media source | | | ## Properties `color` - the type of the component A unique internal-only identifier that is posted to an app's plugin server to share values inputted by the creator Creator-facing identifier that is shown in the plugin environment Default color value that is pre-filled when the component first loads (e.g. `"#ff0000"`) Determines whether the creator must select a color before proceeding Brief creator-facing explanation that clarifies the component's purpose and usage. (256 character limit. Supports markdown: `[text](url)`, `**bold**`, and `*italic*`) When set to `true`, displays a "Transparent" toggle that allows creators to set the color to transparent. Defaults to `false`. Allows for the field to be shown conditionally. dependent on other fields. See [dependencies page](/plugins/component-library/dependencies) for more details. ## Best practices ### Automatic styling When using the `color picker` for content blocks, alongside the settings object, Kit also shares data on the styles used within the email template - allowing your plugin to assume the styling of the email automatically, to make it feel as native as possible. Details on the style data available can be found below:Name of the dependent field Value for the dependent field required to show this field. To show when any value is inputted, leave out this property. * p * h1 * h2 * h3 * h4 * h5 * h6 * ol * ul * blockquote * a * "color", * "font-family", * "font-size", * "font-weight", * "letter-spacing", * "line-height", * "text-align", * "text-transform", * "margin-top", * "margin-right", * "margin-bottom", * "margin-left", * "padding-top", * "padding-right", * "padding-bottom", * "padding-left" ```json theme={null} { "settings": { "postId": "default-to-generosity-id-123", "favoriteColor": "#ff0000" }, "styles": { "p": { "color": "rgb(45, 45, 47)", "font-family": "Charter, Georgia, Times, \"Times New Roman\", serif", "font-size": "18px", "font-weight": "400", "letter-spacing": "normal", "line-height": "27px", "text-align": "start", "text-transform": "none", "margin-top": "18px", "margin-right": "0px", "margin-bottom": "18px", "margin-left": "0px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" }, "h1": { "color": "rgb(0, 0, 0)", "font-family": "-apple-system, \"system-ui\", \"Segoe UI\", Roboto, Oxygen-Sans, Ubuntu, Cantarell, \"Helvetica Neue\", sans-serif", "font-size": "28px", "font-weight": "700", "letter-spacing": "normal", "line-height": "42px", "text-align": "start", "text-transform": "none", "margin-top": "0px", "margin-right": "0px", "margin-bottom": "0px", "margin-left": "0px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" }, "h2": { "color": "rgb(0, 0, 0)", "font-family": "-apple-system, \"system-ui\", \"Segoe UI\", Roboto, Oxygen-Sans, Ubuntu, Cantarell, \"Helvetica Neue\", sans-serif", "font-size": "21px", "font-weight": "700", "letter-spacing": "normal", "line-height": "31.5px", "text-align": "start", "text-transform": "none", "margin-top": "21px", "margin-right": "0px", "margin-bottom": "21px", "margin-left": "0px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" }, "h3": { "color": "rgb(0, 0, 0)", "font-family": "-apple-system, \"system-ui\", \"Segoe UI\", Roboto, Oxygen-Sans, Ubuntu, Cantarell, \"Helvetica Neue\", sans-serif", "font-size": "16.38px", "font-weight": "700", "letter-spacing": "normal", "line-height": "24.57px", "text-align": "start", "text-transform": "none", "margin-top": "16.38px", "margin-right": "0px", "margin-bottom": "16.38px", "margin-left": "0px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" }, "h4": { "color": "rgb(0, 0, 0)", "font-family": "-apple-system, \"system-ui\", \"Segoe UI\", Roboto, Oxygen-Sans, Ubuntu, Cantarell, \"Helvetica Neue\", sans-serif", "font-size": "14px", "font-weight": "700", "letter-spacing": "normal", "line-height": "21px", "text-align": "start", "text-transform": "none", "margin-top": "0px", "margin-right": "0px", "margin-bottom": "0px", "margin-left": "0px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" }, "h5": { "color": "rgb(0, 0, 0)", "font-family": "-apple-system, \"system-ui\", \"Segoe UI\", Roboto, Oxygen-Sans, Ubuntu, Cantarell, \"Helvetica Neue\", sans-serif", "font-size": "14px", "font-weight": "700", "letter-spacing": "normal", "line-height": "15.4px", "text-align": "start", "text-transform": "none", "margin-top": "0px", "margin-right": "0px", "margin-bottom": "0px", "margin-left": "0px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" }, "h6": { "color": "rgb(0, 0, 0)", "font-family": "-apple-system, \"system-ui\", \"Segoe UI\", Roboto, Oxygen-Sans, Ubuntu, Cantarell, \"Helvetica Neue\", sans-serif", "font-size": "14px", "font-weight": "400", "letter-spacing": "normal", "line-height": "21px", "text-align": "start", "text-transform": "none", "margin-top": "0px", "margin-right": "0px", "margin-bottom": "0px", "margin-left": "0px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" }, "ol": { "color": "rgb(45, 45, 47)", "font-family": "Charter, Georgia, Times, \"Times New Roman\", serif", "font-size": "18px", "font-weight": "400", "letter-spacing": "normal", "line-height": "27px", "text-align": "start", "text-transform": "none", "margin-top": "18px", "margin-right": "0px", "margin-bottom": "18px", "margin-left": "18px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" }, "ul": { "color": "rgb(45, 45, 47)", "font-family": "Charter, Georgia, Times, \"Times New Roman\", serif", "font-size": "18px", "font-weight": "400", "letter-spacing": "normal", "line-height": "27px", "text-align": "start", "text-transform": "none", "margin-top": "18px", "margin-right": "0px", "margin-bottom": "18px", "margin-left": "18px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" }, "blockquote": { "color": "rgb(55, 63, 69)", "font-family": "\"open sans\", \"helvetica neue\", Helvetica, Arial, sans-serif", "font-size": "17.5px", "font-weight": "400", "letter-spacing": "normal", "line-height": "25px", "text-align": "start", "text-transform": "none", "margin-top": "17.5px", "margin-right": "0px", "margin-bottom": "17.5px", "margin-left": "0px", "padding-top": "10px", "padding-right": "20px", "padding-bottom": "10px", "padding-left": "20px" }, "a": { "color": "rgb(32, 177, 150)", "font-family": "\"open sans\", \"helvetica neue\", Helvetica, Arial, sans-serif", "font-size": "14px", "font-weight": "400", "letter-spacing": "normal", "line-height": "20px", "text-align": "start", "text-transform": "none", "margin-top": "0px", "margin-right": "0px", "margin-bottom": "0px", "margin-left": "0px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" } } } ``` ```json JSON setting theme={null} { "type": "color", "name": "background_color", "label": "Background color", "default": "#ff0000", // optional "required": true, // optional "help": "help text shown in tooltip to creator while editing", // optional "allow_transparent": true, // optional "dependencies": [ { "field": "dependent_field", "value": "dependent_value" //optional } ] // optional } ``` ```json Example response theme={null} { "settings": { "background_color": "#C8102E" // ...additional plugin settings } // ...plugin-specific additional data } ``` # Date picker Source: https://developers.kit.com/plugins/component-library/date-picker The date picker allows creators to select specific dates for time-sensitive content such as event announcements, promotional campaigns, or content scheduling. All dates are standardized and returned in UTC ISO8601 format to ensure consistent handling across different time zones and applications.## Compatibility | Plugin type | Availability | Additional notes | | -------------- | ---------------------------- | ---------------- | | Content blocks |
| | | Media source | | | ## Properties `date` - the type of the component A unique internal-only identifier that is posted to an app's plugin server to share values inputted by the creator Creator-facing identifier that is shown in the plugin environment Default date value in UTC ISO8601 format that is pre-filled when the component first loads (e.g. `"2026-01-01T00:00:00.000Z"`) Determines whether the creator must select a date before proceeding Brief creator-facing explanation that clarifies the component's purpose and usage. (256 character limit. Supports markdown: `[text](url)`, `**bold**`, and `*italic*`) Allows for the field to be shown conditionally. Dependent on other fields. See [dependencies page](/plugins/component-library/dependencies) for more details. Name of the dependent field Value for the dependent field required to show this field. To show when any value is inputted, leave out this property. ```json JSON setting theme={null} { "type": "date", "name": "start_date", "label": "Start date", "default": "2026-01-01T00:00:00.000Z", // optional "required": false, // optional "help": "help text shown in tooltip to creator while editing", // optional "dependencies": [ { "field": "dependent_field", "value": "dependent_value" //optional } ] // optional } ``` ```json Example response theme={null} { "settings": { "start_date": "2024-10-03T07:00:00.000Z" // ...additional plugin settings } // ...plugin-specific additional data } ``` # Dependencies Source: https://developers.kit.com/plugins/component-library/dependencies Many Kit components support **dependencies** — a feature that conditionally displays customization options based on user interactions with other fields. This creates a more intuitive and contextual user experience by showing relevant options only when needed. ## Configuration To implement dependencies, add a `dependencies` property to your component's JSON configuration. This property accepts an array of objects that specify which fields must be interacted with before the current field becomes visible. In order for a field to show when any value is selected: ```json theme={null} { "name": "advancedOption", "type": "text", "dependencies": [ { "field": "basicOption" }, { "field": "secondDependency" } ] // ...additional component-specific settings } ``` ## Value-Specific Dependencies For more granular control, you can require specific values using a dependency object, with an additional specified value property. The dependent field will only appear when the specified field contains the exact value. ```json theme={null} { "name": "colorPicker", "type": "color", "dependencies": [ { "field": "customColor", "value": "custom" } ] // ...additional component-specific settings } ``` If you only want to show a component when a field has no value, you can specify the value as `null`: ```json theme={null} { "name": "colorPicker", "type": "color", "dependencies": [ { "field": "customColor", "value": null } ] // ...additional component-specific settings } ``` ## Component-Specific Behaviors While dependencies are widely supported, certain components have unique behaviors or limitations, examples can be found below: * **Toggle components**: Only trigger dependencies when set to `true` (on state) * **Some components**: May not support dependencies due to their functional design For detailed information about dependency support and special behaviors for each component type, refer to the individual component documentation pages. ## Best Practices * Use dependencies to progressively reveal complex options * Keep dependency chains simple and logical * Test all dependency scenarios to ensure expected behavior * Document any custom dependency logic for team members and add to your app's help centre article # Dynamic select input Source: https://developers.kit.com/plugins/component-library/dynamic-select-input The dynamic select input provides creators with a dropdown menu containing options fetched dynamically from your server. This component is specifically designed for filtering media gallery results in real-time. When a creator selects an option, it triggers a new request to refresh the media results based on the selected filter value.![]()
Fields cannot be dependent on dynamic select inputs as they are only available for the media source, that has restricted component functionality. Check the [media source documentation](/plugins/media-source/plugin-settings) for more details. ## Compatibility | Plugin type | Availability | Additional notes | | -------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | Content blocks || | | Media source | | Available as part of the preset `filter` group functionality. Check [media source documentation](/plugins/media-source/plugin-settings) for more details. | ## Request URL behaviour Kit makes POST requests to your `request_url` in the following scenarios: * **Initial load**: Request to populate the dropdown options when the plugin loads * **Filter selection**: Request with the selected value to return filtered media results Your endpoint should return an array of label-value pairs for the dropdown options: ```json theme={null} { "options": [ { "label": "Home", "value": "home" }, { "label": "Favorites", "value": "favorites" }, { "label": "Shared", "value": "shared" } ] } ``` ## Properties `dynamicSelect` - the type of the component A unique internal-only identifier that is posted to an app's plugin server to share values inputted by the creator Creator-facing identifier that is shown in the plugin environment The endpoint URL that Kit will call to fetch dynamic options and handle filter selections Determines whether the creator must make a selection before proceeding ```json JSON setting theme={null} { "type": "group", "name": "filter_group", "settings": [ { "type": "dynamicSelect", "label": "Folders", "name": "folder", "request_url": "https://example-plugin.com/folders", "required": false } ] } ``` ```json Example response theme={null} { "settings": { "folder": "favorites" // ...additional plugin settings } // ...plugin-specific additional data } ``` # Font picker Source: https://developers.kit.com/plugins/component-library/font-picker The font picker allows creators to select typography that matches their brand identity and enhances readability. It provides access to email-safe font families with their appropriate fallbacks, along with font weight options to create visual hierarchy and emphasis in email content.## Compatibility | Plugin type | Availablity | Additional notes | | -------------- | ---------------------------- | ---------------- | | Content blocks |
| | | Media source | | | ## Properties `fontFamily` - the type of the component A unique internal-only identifier that is posted to an app's plugin server to share values inputted by the creator Creator-facing identifier that is shown in the plugin environment Default font value that is pre-selected when the component first loads. Object with `fontFamily` (string) and `fontWeight` (number or string) properties. Determines whether the creator must select a font before proceeding Brief creator-facing explanation that clarifies the component's purpose and usage. (256 character limit. Supports markdown: `[text](url)`, `**bold**`, and `*italic*`) Allows for the field to be shown conditionally. dependent on other fields. See [dependencies page](/plugins/component-library/dependencies) for more details. ## Best practices ### Automatic styling When using the `font picker` for content blocks, alongside the settings object, Kit also shares data on the styles used within the email template - allowing your plugin to assume the styling of the email automatically, to make it feel as native as possible. Details on the style data available can be found below:Name of the dependent field Value for the dependent field required to show this field. To show when any value is inputted, leave out this property. * p * h1 * h2 * h3 * h4 * h5 * h6 * ol * ul * blockquote * a * "color", * "font-family", * "font-size", * "font-weight", * "letter-spacing", * "line-height", * "text-align", * "text-transform", * "margin-top", * "margin-right", * "margin-bottom", * "margin-left", * "padding-top", * "padding-right", * "padding-bottom", * "padding-left" ```json theme={null} { "settings": { "postId": "default-to-generosity-id-123", "favoriteColor": "#ff0000" }, "styles": { "p": { "color": "rgb(45, 45, 47)", "font-family": "Charter, Georgia, Times, \"Times New Roman\", serif", "font-size": "18px", "font-weight": "400", "letter-spacing": "normal", "line-height": "27px", "text-align": "start", "text-transform": "none", "margin-top": "18px", "margin-right": "0px", "margin-bottom": "18px", "margin-left": "0px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" }, "h1": { "color": "rgb(0, 0, 0)", "font-family": "-apple-system, \"system-ui\", \"Segoe UI\", Roboto, Oxygen-Sans, Ubuntu, Cantarell, \"Helvetica Neue\", sans-serif", "font-size": "28px", "font-weight": "700", "letter-spacing": "normal", "line-height": "42px", "text-align": "start", "text-transform": "none", "margin-top": "0px", "margin-right": "0px", "margin-bottom": "0px", "margin-left": "0px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" }, "h2": { "color": "rgb(0, 0, 0)", "font-family": "-apple-system, \"system-ui\", \"Segoe UI\", Roboto, Oxygen-Sans, Ubuntu, Cantarell, \"Helvetica Neue\", sans-serif", "font-size": "21px", "font-weight": "700", "letter-spacing": "normal", "line-height": "31.5px", "text-align": "start", "text-transform": "none", "margin-top": "21px", "margin-right": "0px", "margin-bottom": "21px", "margin-left": "0px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" }, "h3": { "color": "rgb(0, 0, 0)", "font-family": "-apple-system, \"system-ui\", \"Segoe UI\", Roboto, Oxygen-Sans, Ubuntu, Cantarell, \"Helvetica Neue\", sans-serif", "font-size": "16.38px", "font-weight": "700", "letter-spacing": "normal", "line-height": "24.57px", "text-align": "start", "text-transform": "none", "margin-top": "16.38px", "margin-right": "0px", "margin-bottom": "16.38px", "margin-left": "0px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" }, "h4": { "color": "rgb(0, 0, 0)", "font-family": "-apple-system, \"system-ui\", \"Segoe UI\", Roboto, Oxygen-Sans, Ubuntu, Cantarell, \"Helvetica Neue\", sans-serif", "font-size": "14px", "font-weight": "700", "letter-spacing": "normal", "line-height": "21px", "text-align": "start", "text-transform": "none", "margin-top": "0px", "margin-right": "0px", "margin-bottom": "0px", "margin-left": "0px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" }, "h5": { "color": "rgb(0, 0, 0)", "font-family": "-apple-system, \"system-ui\", \"Segoe UI\", Roboto, Oxygen-Sans, Ubuntu, Cantarell, \"Helvetica Neue\", sans-serif", "font-size": "14px", "font-weight": "700", "letter-spacing": "normal", "line-height": "15.4px", "text-align": "start", "text-transform": "none", "margin-top": "0px", "margin-right": "0px", "margin-bottom": "0px", "margin-left": "0px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" }, "h6": { "color": "rgb(0, 0, 0)", "font-family": "-apple-system, \"system-ui\", \"Segoe UI\", Roboto, Oxygen-Sans, Ubuntu, Cantarell, \"Helvetica Neue\", sans-serif", "font-size": "14px", "font-weight": "400", "letter-spacing": "normal", "line-height": "21px", "text-align": "start", "text-transform": "none", "margin-top": "0px", "margin-right": "0px", "margin-bottom": "0px", "margin-left": "0px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" }, "ol": { "color": "rgb(45, 45, 47)", "font-family": "Charter, Georgia, Times, \"Times New Roman\", serif", "font-size": "18px", "font-weight": "400", "letter-spacing": "normal", "line-height": "27px", "text-align": "start", "text-transform": "none", "margin-top": "18px", "margin-right": "0px", "margin-bottom": "18px", "margin-left": "18px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" }, "ul": { "color": "rgb(45, 45, 47)", "font-family": "Charter, Georgia, Times, \"Times New Roman\", serif", "font-size": "18px", "font-weight": "400", "letter-spacing": "normal", "line-height": "27px", "text-align": "start", "text-transform": "none", "margin-top": "18px", "margin-right": "0px", "margin-bottom": "18px", "margin-left": "18px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" }, "blockquote": { "color": "rgb(55, 63, 69)", "font-family": "\"open sans\", \"helvetica neue\", Helvetica, Arial, sans-serif", "font-size": "17.5px", "font-weight": "400", "letter-spacing": "normal", "line-height": "25px", "text-align": "start", "text-transform": "none", "margin-top": "17.5px", "margin-right": "0px", "margin-bottom": "17.5px", "margin-left": "0px", "padding-top": "10px", "padding-right": "20px", "padding-bottom": "10px", "padding-left": "20px" }, "a": { "color": "rgb(32, 177, 150)", "font-family": "\"open sans\", \"helvetica neue\", Helvetica, Arial, sans-serif", "font-size": "14px", "font-weight": "400", "letter-spacing": "normal", "line-height": "20px", "text-align": "start", "text-transform": "none", "margin-top": "0px", "margin-right": "0px", "margin-bottom": "0px", "margin-left": "0px", "padding-top": "0px", "padding-right": "0px", "padding-bottom": "0px", "padding-left": "0px" } } } ``` ```json JSON setting theme={null} { "type": "fontFamily", "name": "paragraph_font", "label": "Paragraph font", "default": { // optional "fontFamily": "'Courier New', Courier, monospace", "fontWeight": 400 }, "required": true, // optional "help": "help text shown in tooltip to creator while editing", // optional "dependencies": [ { "field": "dependent_field", "value": "dependent_value" //optional } ] // optional } ``` ```json Example response theme={null} { "settings": { "paragraph_font": { "fontFamily": "'Courier New', Courier, monospace", "fontWeight": 400 } // ...additional plugin settings } // ...plugin-specific additional data } ``` # Group Source: https://developers.kit.com/plugins/component-library/group The group component provides a collapsible organizational structure for grouping related plugin settings together. It creates an expandable/collapsible section with a clear label and optional help text, making it easy for app developers to manage complex plugin configurations by organizing them into logical groups. The component allows for better user experience when dealing with plugins that have many settings.## Compatibility | Plugin type | Availability | Additional notes | | -------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | Content blocks |
| | | Media source | | Available as part of the preset `search` functionality. Check [media source documentation](/plugins/media-source/plugin-settings) for more details. | ## Properties `group` - the type of the component Creator-facing identifier that is shown as the group header in the plugin environment Array of child setting objects that will be displayed within the collapsed group. Each setting should be a valid plugin setting component. Brief creator-facing explanation that clarifies the group's purpose and usage. (256 character limit. Supports markdown: `[text](url)`, `**bold**`, and `*italic*`) Allows for the group to be shown conditionally, dependent on other fields. See [dependencies page](/plugins/component-library/dependencies) for more details. ## Testing Examples ### Simple Group (Copy & Paste Ready) ```json theme={null} { "type": "group", "label": "Basic Settings", "name": "basic_settings", "settings": [ { "type": "text", "name": "title", "label": "Title" }, { "type": "text", "name": "description", "label": "Description" } ] } ``` ### Group with Help Text ```json theme={null} { "type": "group", "label": "Advanced Settings", "name": "basic_settings", "help": "Configure advanced options here", "settings": [ { "type": "text", "name": "api_key", "label": "API Key", "placeholder": "Enter your API key" }, { "type": "select", "name": "timeout", "label": "Timeout", "options": [ { "label": "5 seconds", "value": "5" }, { "label": "10 seconds", "value": "10" } ] } ] } ``` ### Group with Dependencies ```json theme={null} { "type": "group", "label": "Optional Features", "name": "basic_settings", "settings": [ { "type": "text", "name": "webhook_url", "label": "Webhook URL" } ], "dependencies": [ { "field": "enable_webhooks", "value": "true" } ] } ```Name of the dependent field Value for the dependent field required to show this group. To show when any value is inputted, leave out this property. ```json Example response theme={null} { "settings": { "title": "My Plugin Title", "description": "Plugin description here", "api_key": "abc123xyz", "timeout": "10", "webhook_url": "https://example.com/webhook" } } ``` # Numerical input Source: https://developers.kit.com/plugins/component-library/numerical-input The numerical input allows creators to enter numeric values in a dedicated field with optional currency or unit indicators. It's perfect for collecting prices, quantities, measurements, or any numeric data that benefits from contextual prefixes or suffixes. The input provides a clean interface optimized for numerical data entry.## Compatibility | Plugin type | Availability | Additional notes | | -------------- | ---------------------------- | ---------------- | | Content blocks |
| | | Media source | | | ## Properties `numericalInput` - the type of the component A unique internal-only identifier that is posted to an app's plugin server to share values inputted by the creator Creator-facing identifier that is shown in the plugin environment Text or symbol displayed before the input field (e.g., "\$" for currency). Maximum 5 characters. Cannot be used with `append`. Text or symbol displayed after the input field (e.g., "lbs" for weight). Maximum 5 characters. Cannot be used with `prepend`. Maximum value allowed Minimum value allowed Default numeric value when the component first loads Determines whether the creator must enter a value before proceeding Brief creator-facing explanation that clarifies the component's purpose and usage. (256 character limit. Supports markdown: `[text](url)`, `**bold**`, and `*italic*`) Allows for the field to be shown conditionally. dependent on other fields. See [dependencies page](/plugins/component-library/dependencies) for more details. ## Important Notes * Either `prepend` or `append` must be provided, but not both * The placeholder is automatically set to "0" and cannot be customized * Input accepts decimal numbers and automatically validates numeric formatName of the dependent field Value for the dependent field required to show this field. To show when any value is inputted, leave out this property. ```json JSON setting with prepend theme={null} { "type": "numericalInput", "name": "price", "label": "Product Price", "prepend": "$", "max": 200, //optional "min": 0, //optional "default": 3, // optional "required": true, "help": "Enter the price in USD", "dependencies": [ { "field": "enable_pricing", "value": true } ] } ``` ```json Example response theme={null} { "settings": { "price": "99.99", "weight": "2.5" } } ``` # Overview Source: https://developers.kit.com/plugins/component-library/overview Plugin components are the configurable UI elements that creators use to customize their plugins' behavior and appearance in Kit's different plugin environments. These reusable settings are defined in your plugin's JSON configuration and appear when creators interact with your plugin, allowing them to personalize the content and functionality before it's added to their Kit.The color picker lets creators customize colors for backgrounds, buttons, text and more to match their brand and email templates. The date picker lets creators select specific dates for time-sensitive content like events or promotions, with dates returned in standardized UTC format. The dynamic select provides creators with a dropdown of server-fetched options to filter media gallery results in real-time. The font picker lets creators select email-safe fonts and weights to match their brand and improve readability. The group component allows creators to organize settings into collapsible sections for better clarity in complex plugin configurations. The numerical input lets creators enter numeric values with optional currency or unit indicators. The radio group lets creators choose from mutually exclusive options like alignment or layout modes through selectable buttons. The search input lets creators search and select dynamic content from external sources with real-time functionality, supporting single or multiple selections. The select input provides creators with a dropdown of predefined options like categories or sizes, with the selected value passed to your plugin. The slider lets creators select numerical values for properties like dimensions or quantities through a visual slider or direct input for precision. The textarea lets creators enter multi-line text content like descriptions or messages in an expandable interface. The text input lets creators enter short text like titles, names, or URLs in a single-line field. # Radio group Source: https://developers.kit.com/plugins/component-library/radio-group The radio group component allows creators to choose from mutually exclusive options presented as individual selectable buttons. It's ideal for settings where only one choice can be made at a time, such as text alignment, layout options, or display modes. The visual button interface makes selection clear and intuitive for creators. The toggle lets creators enable or disable features through a simple on/off switch with clear visual feedback. ![]()
To get the most out of dependency with radio-groups utilize the `value` option whereby a field only shows when the corresponding value is selected, using the following syntax: ```json theme={null} { "name": "text_alignment", "label": "Text Alignment", "type": "radioGroup", // ...additional component-specific settings "dependencies": [ { "field": "field_name", "value": "expected_value" } ] } ``` A typical use case for this would be to create three pre-defined options and a final `Custom` button, that exposes more granular functionality. ## Compatibility | Plugin type | Availability | Additional notes | | -------------- | ---------------------------- | ---------------- | | Content blocks || | | Media source | | | ## Properties `radioGroup` - the type of the component A unique internal-only identifier that is posted to an app's plugin server to share values inputted by the creator Creator-facing identifier that is shown in the plugin environment (64 character limit) Array of objects containing label-value pairs for the radio button options A unique internal-only identifier that is posted to an app's plugin server to share values from the specific button selected by the creator Creator-facing identifier that is shown on the button (character length across all labels in the component cannot exceed 28 characters) Default option value that is pre-selected when the component first loads Determines whether the creator must make a selection before proceeding (defaults to false) Brief creator-facing explanation that clarifies the component's purpose and usage (256 character limit. Supports markdown: `[text](url)`, `**bold**`, and `*italic*`) Allows for the field to be shown conditionally. dependent on other fields. See [dependencies page](/plugins/component-library/dependencies) for more details. Name of the dependent field Value for the dependent field required to show this field. To show when any value is inputted, leave out this property. ```json JSON setting theme={null} { "name": "text_alignment", "label": "Text Alignment", "required": false, // optional - defaults to false "help": "Choose how text should be aligned", // optional "type": "radioGroup", "default": "left", // optional "options": [ { "label": "Left", "value": "left" }, { "label": "Center", "value": "center" }, { "label": "Right", "value": "right" } ], "dependencies": [ { "field": "dependent_field", "value": "dependent_value" //optional } ] // optional } ``` ```json Example response theme={null} { "settings": { "radio_group": "center" // ...additional plugin settings } // ...plugin-specific additional data } ``` # Search input Source: https://developers.kit.com/plugins/component-library/search-input The search input allows creators to search and select from dynamic content sourced from external APIs or databases. It provides real-time search functionality with debounced requests, supports dependencies on other form inputs, and can handle both single and multiple selections. This component is ideal for selecting blog posts, products, categories, or any searchable content.## Compatibility | Plugin type | Availablity | Additional notes | | -------------- | ---------------------------- | ---------------- | | Content blocks |
| | | Media source | | | ## Request URL behaviour The behaviour of the `request_url` property is specific to the plugin environment in which it is used. The sections below outline the differences between plugin environments: ## Properties * **Initial load**: When a creator adds a content block, Kit makes a request with an empty search parameter to populate default results * **User typing**: Debounced request made by Kit with the user's search query * **Draft restoration**: If a creator leaves their email in a draft state and edits again in the future, we’ll make a request to your provided request\_url with the value of the option they had previously selected. This allows us to fill the dropdown with your user-friendly label. Your endpoint should return an array of label-value pairs: ```json theme={null} { "data": [ { "label": "A post title", "value": "post-id-123" } ] } ``` For error handling, include an errors array: ```json theme={null} { "data": [], "errors": ["Plan not found"] } ``` COMING SOON `search` - the type of the component A unique internal-only identifier that is posted to an app's plugin server to share values inputted by the creator Creator-facing identifier that is shown in the plugin environment The endpoint URL that Kit will call to fetch search results Placeholder text displayed in the search input field Determines whether the creator must make a selection before proceeding Brief creator-facing explanation that clarifies the component's purpose and usage. (256 character limit. Supports markdown: `[text](url)`, `**bold**`, and `*italic*`) Allows for the field to be shown conditionally. dependent on other fields. See [dependencies page](/plugins/component-library/dependencies) for more details. We maintain backwards compatibility for string arrays but recommend using the object format going forwards. Name of the dependent field Value for the dependent field required to show this field. To show when any value is inputted, leave out this property. Enables multiple selection when set to true ## Best practices * Ensure the initial empty state is handled, offering default options when no input is given * Use a sensible sort order for your data - for example alphabetical, most recently created or most popular * Ensure labels are clear, short and unique so that creators know exactly what they are selecting```json JSON setting theme={null} { "type": "search", "name": "post", "label": "Post", "request_url": "https://example.com/path/to/your/search/endpoint", "placeholder": "Select a post", // optional "required": true, // optional "help": "help text shown in tooltip to creator while editing", // optional "multiselect": false, // optional "dependencies": [ { "field": "dependent_field", "value": "dependent_value" //optional } ] // optional } ``` ```json Example response (single select) theme={null} { "settings": { "post": "post-id-123" // ...additional plugin settings } // ...plugin-specific additional data } ``` ```json (multiple select) theme={null} { "settings": { "post": ["post-id-123", "post-id-456"] // ...additional plugin settings } // ...plugin-specific additional data } ``` # Select input Source: https://developers.kit.com/plugins/component-library/select-input The select input provides creators with a dropdown menu containing predefined options. It's ideal for scenarios where you need to present a fixed set of choices such as categories, sizes, or configuration options. The selected value is hidden from creators but passed to your plugin for processing.If you want to generate dynamic options, please use a [search input](/plugins/component-library/search-input) instead. ## Compatibility | Plugin type | Availability | Additional notes | | -------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Content blocks |
| | | Media source | | Available as part of the preset `filter` and `sort` group functionality. Check [media source documentation](/plugins/media-source/plugin-settings) for more details. | ## Properties `select` - the type of the component A unique internal-only identifier that is posted to an app's plugin server to share values inputted by the creator Creator-facing identifier that is shown in the plugin environment Array of objects containing label-value pairs for the dropdown options Placeholder text displayed when no option is selected Default option value that is pre-selected when the component first loads Determines whether the creator must make a selection before proceeding Brief creator-facing explanation that clarifies the component's purpose and usage. (256 character limit. Supports markdown: `[text](url)`, `**bold**`, and `*italic*`) Allows for the field to be shown conditionally. dependent on other fields. See [dependencies page](/plugins/component-library/dependencies) for more details. Name of the dependent field Value for the dependent field required to show this field. To show when any value is inputted, leave out this property. ```json JSON setting theme={null} { "type": "select", "name": "favorite_food", "label": "Favorite food", "options": [ { "label": "French fries", "value": "food-id-1" }, { "label": "Hash browns", "value": "food-id-2" }, { "label": "Potato chips", "value": "food-id-3" } ], "placeholder": "Select a food...", // optional "default": "food-id-1", // optional "required": true, // optional "help": "help text shown in tooltip to creator while editing", // optional "dependencies": [ { "field": "dependent_field", "value": "dependent_value" //optional } ] // optional } ``` ```json Example response theme={null} { "settings": { "favorite_food": "food-id-2" // ...additional plugin settings } // ...plugin-specific additional data } ``` # Slider Source: https://developers.kit.com/plugins/component-library/slider The slider provides creators with an intuitive way to select numerical values through a visual slider interface combined with a numerical input box. It's perfect for precise control over design properties like corner radius, border width, pixel dimensions, or quantities. The dual interface allows creators to either drag the slider for quick adjustments or type exact values for precision.![]()
Fields cannot be dependent on sliders as they always have a value. ## Compatibility | Plugin type | Availability | Additional notes | | -------------- | ---------------------------- | ---------------- | | Content blocks || | | Media source | | | ## Properties `slider` - the type of the component A unique internal-only identifier that is posted to an app's plugin server to share values inputted by the creator Creator-facing identifier that is shown in the plugin environment (64 character limit) Maximum value allowed on the slider Minimum value allowed on the slider Increment value for slider movement and input validation Default value when the component first loads Determines whether the creator must set a value before proceeding, defaults to `false` Brief creator-facing explanation that clarifies the component's purpose and usage (256 character limit. Supports markdown: `[text](url)`, `**bold**`, and `*italic*`) Unit or label displayed after the value (3 character limit, defaults to null). If `null` or absent, the suffix won't show Allows for the field to be shown conditionally. dependent on other fields. See [dependencies page](/plugins/component-library/dependencies) for more details. Name of the dependent field Value for the dependent field required to show this field. To show when any value is inputted, leave out this property. ```json JSON setting theme={null} { "name": "border_width", "label": "Border Width", "required": false, // optional "help": "Set the border width in pixels", // optional "type": "slider", "max": 20, "min": 0, "step": 1, "default": 2, "suffix": "px", // optional "dependencies": [ { "field": "dependent_field", "value": "dependent_value" //optional } ] // optional } ``` ```json Example response theme={null} { "settings": { "border_width": 5 // ...additional plugin settings } // ...plugin-specific additional data } ``` # Text input Source: https://developers.kit.com/plugins/component-library/text-input The text input allows creators to enter short snippets of text in a single-line field. It's perfect for collecting titles, names, URLs, or any brief text content that doesn't require multiple lines. The input provides a clean, straightforward interface for essential text-based information.## Compatibility | Plugin type | Availability | Additional notes | | -------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | Content blocks |
| | | Media source | | Available as part of the preset `search` functionality. Check [media source documentation](/plugins/media-source/plugin-settings) for more details. | ## Properties `text` - the type of the component A unique internal-only identifier that is posted to an app's plugin server to share values inputted by the creator Creator-facing identifier that is shown in the plugin environment Placeholder text displayed in the input field when empty Default text value that is pre-filled when the component first loads Determines whether the creator must enter text before proceeding Brief creator-facing explanation that clarifies the component's purpose and usage. (256 character limit. Supports markdown: `[text](url)`, `**bold**`, and `*italic*`) Allows for the field to be shown conditionally. dependent on other fields. See [dependencies page](/plugins/component-library/dependencies) for more details. Name of the dependent field Value for the dependent field required to show this field. To show when any value is inputted, leave out this property. ```json JSON setting theme={null} { "type": "text", "name": "title", "label": "Title", "placeholder": "Enter a title...", // optional "default": "Buy Now", // optional "required": true, // optional "help": "help text shown in tooltip to creator while editing", // optional "dependencies": [ { "field": "dependent_field", "value": "dependent_value" //optional } ] // optional } ``` ```json Example response theme={null} { "settings": { "title": "My Amazing Email Campaign" // ...additional plugin settings } // ...plugin-specific additional data } ``` # Textarea Source: https://developers.kit.com/plugins/component-library/textarea The textarea component allows creators to enter longer, multi-line text content. It's ideal for collecting descriptions, messages, body content, or any text that requires multiple lines and more space than a single-line input. The textarea provides an expandable interface that accommodates extensive text-based information.## Compatibility | Plugin type | Availability | Additional notes | | -------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | Content blocks |
| | | Media source | | Available as part of the preset `search` functionality. Check [media source documentation](/plugins/media-source/plugin-settings) for more details. | ## Properties `textarea` - the type of the component A unique internal-only identifier that is posted to an app's plugin server to share values inputted by the creator Creator-facing identifier that is shown in the plugin environment (64 character limit) Placeholder text displayed in the textarea when empty Default value when the component first loads Maximum character limit for the textarea content. Set to `null` for unlimited length Determines whether the creator must enter text before proceeding Brief creator-facing explanation that clarifies the component's purpose and usage (256 character limit. Supports markdown: `[text](url)`, `**bold**`, and `*italic*`) Allows for the field to be shown conditionally, dependent on other fields. See [dependencies page](/plugins/component-library/dependencies) for more details. Name of the dependent field Value for the dependent field required to show this field. To show when any value is inputted, leave out this property. ```json JSON setting theme={null} { "type": "textarea", "name": "feature_description", "label": "Feature Description", "placeholder": "Describe the feature when enabled", // optional "default": "This text describes the feature", // optional "max_length": null, // optional - null for unlimited "required": false, // optional "help": "Only appears when feature toggle is enabled", // optional "dependencies": [ { "field": "enable_feature" // no value property - shows when enable_feature has any value } ] // optional } ``` ```json Example response theme={null} { "settings": { "feature_description": "This feature enables advanced analytics tracking for user interactions, providing detailed insights into engagement patterns and conversion metrics. It includes real-time monitoring, custom event tracking, and comprehensive reporting dashboards." // ...additional plugin settings } // ...plugin-specific additional data } ``` # Toggle Source: https://developers.kit.com/plugins/component-library/toggle The toggle allows creators to enable or disable specific features through a simple boolean switch interface. It's perfect for optional configurations like custom descriptions, showing or hiding elements, applying filters, or turning functionality on and off. The toggle provides clear visual feedback about the current state of the setting.![]()
Fields dependent on toggles will only show when the toggle is switched on (i.e. has a value `true`). ## Compatibility | Plugin type | Availability | Additional notes | | -------------- | ---------------------------- | ---------------- | | Content blocks || | | Media source | | | ## Properties `toggle` - the type of the component A unique internal-only identifier that is posted to an app's plugin server to share values inputted by the creator Creator-facing identifier that is shown in the plugin environment (64 character limit) Default state of the toggle when the component first loads Determines whether the creator must interact with the toggle before proceeding Brief creator-facing explanation that clarifies the component's purpose and usage (256 character limit. Supports markdown: `[text](url)`, `**bold**`, and `*italic*`) Allows for the field to be shown conditionally. dependent on other fields. See [dependencies page](/plugins/component-library/dependencies) for more details. Name of the dependent field Value for the dependent field required to show this field. To show when any value is inputted, leave out this property. ```json JSON setting theme={null} { "type": "toggle", "name": "optional_toggle", "label": "Optional Toggle", "default": false, // optional "required": false, // optional "help": "Helping you understand what this toggle does", //optional "dependencies": [ { "field": "dependent_field", "value": "dependent_value" //optional } ] // optional } ``` ```json Example response theme={null} { "settings": { "optional_toggle": true // ...additional plugin settings } // ...plugin-specific additional data } ``` # Liquid templates for dynamic content blocks Source: https://developers.kit.com/plugins/content-blocks/dynamic-blocks/liquid-templates Writing Liquid-templated HTML for dynamic content blocks Dynamic content block HTML is a Liquid template. When Kit sends an email, it renders your template for each subscriber by substituting Liquid variables with that recipient’s data (automation context, subscriber fields, and other send-time drops available in Kit’s email pipeline). ## Variable paths Dynamic content blocks support two data sources, each with its own variable path. ### Automation event data When your block is linked to an automation event plugin, event data is accessed through the `automation` namespace: ``` automation.{app_identifier}.{event_node_identifier}.{field_name} ``` * **`app_identifier`** — Your app's unique identifier, assigned in the Kit App Store. * **`event_node_identifier`** — The `identifier` of the **linked automation event plugin element** (the event node chosen via `related_plugin_id`), not the dynamic content block plugin's own `identifier`. * **`field_name`** — A key from the event data stored for that subscriber (typically aligned with the fields your polling integration puts in each event's `context`). For example, if your app identifier is `yourapp` and the linked event node's `identifier` is `abandoned_checkout`, you'd access the checkout URL like this: ```liquid theme={null} {{ automation.yourapp.abandoned_checkout.checkout_url }} ``` ### Subscriber custom fields Subscriber custom fields are accessible directly via the `subscriber` namespace, regardless of whether the block uses an event node: ``` subscriber.{field_name} ``` For example, to reference a custom field named `company`: ```liquid theme={null} {{ subscriber.company }} ``` You can use subscriber custom fields alongside automation event data in the same template, or as the sole data source in blocks that don't require an event node.In the **email editor preview** for a dynamic content block, Kit renders Liquid using preview context built from `default_values` and (when linked) the `automation` namespace—**not** the full send-time Liquid environment. In particular, **`subscriber` is not injected on that preview path**, so `{{ subscriber.* }}` may render empty in the editor even though it works at send time. Always verify behavior in a **sent test email** when mixing `subscriber` and `automation` variables. *** ## Writing your template ### Guard the entire block Always wrap your content in a top-level conditional. This ensures the block renders gracefully if the automation data isn't present—for example, if the email is sent outside the expected workflow: ```html theme={null} {% if automation.yourapp.abandoned_checkout %} {% endif %} ``` ### Iterate over arrays Use Liquid's `for` tag to loop over arrays. The `limit` filter caps the number of items rendered. Always cap loops in email templates to avoid unexpectedly long emails: ```html theme={null} {% for product in automation.yourapp.abandoned_checkout.products limit: 4 %}{% endfor %} ```
{% if product.image_url %} {% endif %}
{{ product.name }}
{{ product.price }}
**Don't place Liquid control-flow tags (`{% if %}`, `{% for %}`) as direct children of ` `, ``, or `
`.** Browsers and email clients silently move them outside the table per the HTML5 parsing spec, which breaks the template. Wrap each iterated row in its **own** ` `, or move the loop **inside** a `
`: ```html theme={null} {% for product in automation.yourapp.abandoned_checkout.products limit: 4 %} {% endfor %}
{{ product.name }} {% for product in automation.yourapp.abandoned_checkout.products limit: 4 %}
``` When Kit fetches your dynamic block HTML, it scans for this pattern and surfaces a warning on the editor preview and on the **Test fetch HTML** panel in your plugin form. The warning is only shown to the developer who owns the app—creators don't see it. ### Conditionally show fields Use `{% if %}` to guard optional fields. Subscribers may have different data states, so don't assume every field is always populated: ```html theme={null} {% if product.image_url %}{% endfor %} {{ product.name }} {% endif %} {% assign discount_count = automation.yourapp.abandoned_checkout.discount_codes | size %} {% if discount_count > 0 %}
Discount applied: {{ automation.yourapp.abandoned_checkout.discount_codes | join: ", " }}
{% endif %} ``` ### Show overflow counts When you limit the number of items shown, you can tell subscribers how many more they have: ```html theme={null} {% assign total = automation.yourapp.abandoned_checkout.products | size %} {% assign shown = 4 %} {% assign remaining = total | minus: shown %} {% if remaining > 0 %}+ {{ remaining }} more item{% if remaining != 1 %}s{% endif %} in your cart
{% endif %} ``` *** ## Available Liquid filters Kit's email rendering uses Liquid with a **supported subset** of filters (similar to common Liquid, but not identical to every filter documented for other Liquid hosts). Prefer filters you have verified in a **sent test email** or editor preview. Commonly useful ones in dynamic content blocks: | Filter | Example | Output | | ---------- | ---------------------------- | ------------------------------- | | `size` | `{{ products \| size }}` | Number of items in an array | | `minus` | `{{ total \| minus: 4 }}` | Subtraction | | `join` | `{{ codes \| join: ", " }}` | Join array to string | | `truncate` | `{{ name \| truncate: 60 }}` | Truncate string to N characters | | `upcase` | `{{ label \| upcase }}` | Uppercase string | | `downcase` | `{{ label \| downcase }}` | Lowercase string | *** ## Liquid in button URLs Creators can reference the same `automation.*` Liquid variables in regular email button URLs—not just inside dynamic content blocks—on **automation sequence emails**. For example, a creator can set a button's URL to `{{ automation.yourapp.abandoned_checkout.checkout_url }}` and Kit will render the per-subscriber URL at send time. This means a dynamic block isn't the only place your event context becomes useful. Buttons, links, and plain-text body Liquid in automation sequence emails all read from the same `automation.*` namespace your event plugin populates. *** ## Context size limits To protect the send pipeline, Kit applies the following limits to per-subscriber automation context: | Limit | Value | | ------------------------------------------------------------- | --------- | | Total bytes per subscriber context row | **64 KB** | | Distinct app namespaces per subscriber (`automation..*`) | **32** | | Keys per app namespace (events × top-level fields) | **64** | Events that exceed 64 KB on their own are dropped before storage; merges that would push a subscriber's row over the limit are skipped. Other subscribers in the same batch continue to progress through the VA, and the existing context on the row is preserved. Drops are logged but do not surface to the creator. Design your `context` payloads accordingly: prefer pre-trimmed lists (e.g. top 5 items), compact field names, and pre-formatted display strings over raw nested objects. *** ## Preview vs. send time Your template is rendered twice: 1. **At design time (editor preview)** — Kit renders your template using `default_values` merged into the `automation` namespace when an event node is linked (see [plugin configuration](/plugins/content-blocks/dynamic-blocks/plugin-configuration)). Preview does **not** mirror the full send-time Liquid drop (for example, `subscriber` is not available on this preview path). 2. **At send time** — Kit renders the same stored template with real per-recipient data, including automation context from Visual Automations (when applicable) and the normal `subscriber` Liquid drop for subscriber fields. Because of this, your template must handle both cases gracefully. Ensure your `default_values` produce HTML that looks close to what real data would render—creators use this preview to judge how the block will look—and validate anything that depends on `subscriber` in a sent email, not only in the editor. *** ## Email compatibility The same security and compatibility rules that apply to standard content blocks also apply to your rendered Liquid template output. Kit sanitizes the final HTML before delivery. **Avoid these in your template:** * Scripts and iframes * Audio and video elements * Form, input, or command elements * External CSS stylesheets and CSS `url()` values **Best practices:** * Use inline `style` attributes instead of CSS classes—email clients strip external stylesheets * Use table-based layouts (` `, `
`, ` `) for reliable rendering across email clients * Keep your maximum width at 600px * Include `target="_blank"` on all links * Use absolute URLs for all images and links * Guard image fields with `{% if %}` so missing images don't produce broken ` ` tags See [plugin security](/plugins/content-blocks/plugin-security) and [plugin recommendations](/plugins/content-blocks/plugin-recommendations) for the full list of restrictions and best practices. *** ## Full example An abandoned cart block with a product list, totals, and a checkout button: ```html theme={null} {% if automation.yourapp.abandoned_checkout %}
{% for product in automation.yourapp.abandoned_checkout.products limit: 4 %}
{% endif %} ``` # Dynamic content blocks overview Source: https://developers.kit.com/plugins/content-blocks/dynamic-blocks/overview Subscriber-personalized content blocks powered by Liquid templates Dynamic content blocks extend Kit's standard [content block plugins](/plugins/content-blocks/overview) to support subscriber-personalized email content. Rather than returning final, static HTML at design time, your server returns a Liquid-templated HTML response that Kit renders individually for each subscriber at send time. ## How dynamic blocks differ from standard blocks With a standard content block, Kit calls your endpoint whenever a creator configures or refreshes the block in the email editor. Your server returns HTML that is stored as-is and delivered identically to every recipient. With a dynamic content block, Kit still calls your endpoint at design time—but the HTML you return contains [Liquid template variables](/plugins/content-blocks/dynamic-blocks/liquid-templates). Kit stores this template and renders it per subscriber at send time, substituting real data from the chosen data source. | | Standard content blocks | Dynamic content blocks | | ------------------ | ---------------------------- | -------------------------------------------------------------------------------------- | | **HTML generated** | Once, at design time | Template at design time; rendered per subscriber at send time | | **Content** | Identical for all recipients | Personalized per subscriber | | **Works in** | Any email type | Any email type; event node data source requires a Visual Automation sequence email | | **Requires** | Content block plugin | Content block plugin; automation event plugin required only for event node data source | ## Data sources Dynamic content blocks support two data sources: * **Automation event node** — An [automation node plugin](/plugins/automation-nodes/overview) that fires when a subscriber triggers an event (such as abandoning a cart). The event plugin stores subscriber-specific data in Kit's automation context, and your Liquid template references it via `automation.{app_identifier}.{event_node_identifier}.*`, where `event_node_identifier` is the linked event node's `identifier`. Requires a Visual Automation sequence email. * **Subscriber custom fields** — Subscriber data stored directly on the Kit subscriber record and accessible via `{{ subscriber.field_name }}` in your template. Works in any email type. ## When to use dynamic blocks Dynamic content blocks are the right choice when: * Content must reflect data specific to each subscriber at the moment the email is sent * The underlying data changes between when the email is designed and when it is delivered * The block depends on an event a subscriber triggered inside a Visual Automation workflow * You want to personalize content using data already stored on the subscriber record **Common use cases:** * **Abandoned cart recovery** — Show each subscriber the actual items they left in their cart * **Personalized product recommendations** — Surface items relevant to a specific subscriber's history * **Event-triggered summaries** — Display real-time order, booking, or activity details * **Subscriber-specific content** — Greet subscribers by name, reference their plan, or display content based on custom field values ## Prerequisites To build a dynamic content block you need a **dynamic content block plugin**—a content block plugin configured with `dynamic: true` and the additional fields described in [plugin configuration](/plugins/content-blocks/dynamic-blocks/plugin-configuration). If your block uses an **automation event node** as its data source, you also need an automation node plugin in the same Kit app that captures subscriber-specific data and stores it in Kit's automation context. See [plugin flow](/plugins/content-blocks/dynamic-blocks/plugin-flow) for a step-by-step walkthrough of this setup. # Dynamic content blocks plugin configuration Source: https://developers.kit.com/plugins/content-blocks/dynamic-blocks/plugin-configuration Configuring your dynamic content block plugin Dynamic content block plugins share the same base configuration as [standard content block plugins](/plugins/content-blocks/plugin-configuration)—name, description, icon, request URL, and settings JSON—with a few additional fields that power the personalization and preview experience. The sample data that drives the editor preview (and that defines the field names creators can reference in Liquid) is configured on the **linked automation event plugin**, not on the content block plugin itself. See [Sample data lives on the event plugin](#sample-data-lives-on-the-event-plugin) below.{% endfor %} {% assign total_count = automation.yourapp.abandoned_checkout.products | size %} {% assign remaining = total_count | minus: 4 %} {% if remaining > 0 %}
{% if product.image_url %} {% endif %} ![]()
{{ product.name }}
Qty: {{ product.quantity }}
{{ product.price }}
{% endif %} + {{ remaining }} more item{% if remaining != 1 %}s{% endif %} Total: {{ automation.yourapp.abandoned_checkout.total }} Complete your order ![]()
![]()
All UI components available for standard content blocks—text inputs, color pickers, search inputs, and the rest of the [component library](/plugins/component-library/overview)—are available for dynamic content blocks as well. ## Additional configuration fields ### `dynamic` Set to `true` to mark this plugin as a dynamic content block. This tells Kit to treat the HTML returned by your endpoint as a Liquid template rather than final, static HTML. ### `identifier` A short, unique string that identifies this **content block** plugin element within your app (used in the Apps Dashboard and for uniqueness across plugin elements). **Rules:** * Lowercase letters, numbers, and underscores only * Must be unique within your app When the block is linked to an automation event plugin (`related_plugin_id`), Liquid paths use the **linked event node's** `identifier`, not this content block's `identifier`: ``` automation.{app_identifier}.{event_node_identifier}.{field_name} ``` For example, if your app identifier is `yourapp` and the related event node's `identifier` is `abandoned_checkout`, template variables are scoped under `automation.yourapp.abandoned_checkout.*`. The content block and event node identifiers are often named similarly by convention, but they are configured separately—only the event node's `identifier` appears in that middle segment. ### `related_plugin_id` The automation event plugin that provides subscriber-specific data for this block. In the Kit App Store builder, this is selected from a dropdown listing your app's automation event plugins. This links your dynamic content block to the workflow step that captures and stores the data.Your dynamic content block will only render correctly when used in an automation that includes the linked event plugin. The event plugin is responsible for populating the automation context data that your Liquid variables reference. ## Sample data lives on the event plugin When a dynamic content block is linked to an automation event plugin, **both** the list of fields creators can reference in Liquid **and** the sample data used for the editor preview are configured on the **event plugin**, not on the content block plugin. Open your event plugin in **Apps → your app → Plugins → \[event plugin]**. In the **Context** card, paste a JSON object into **Sample data**. The top-level keys become the fields creators can reference in Liquid; the nested values are used to render the preview in the email editor. ```json theme={null} { "products": [ { "name": "Vintage Wool Sweater", "price": "$89.00", "quantity": 1, "image_url": "https://example.com/images/sweater.jpg", "description": "A cozy classic for the cooler months" }, { "name": "Canvas Tote Bag", "price": "$34.00", "quantity": 2, "image_url": "https://example.com/images/tote.jpg", "description": "Sturdy and stylish everyday carry" } ], "checkout_url": "https://example.com/cart/checkout", "subtotal": "$157.00", "total": "$157.00", "discount_codes": [] } ```Make your sample data visually representative. Creators judge how their email will look based on the preview, so use realistic product names, images, and amounts rather than placeholder text. On the dynamic content block plugin form, the same sample data is shown **read-only**, with a **Configure sample data** button that links back to the event plugin. If you need to add a field, edit it on the event plugin and it will become available to every dynamic content block linked to that event.The sample data you configure here is the same data your event plugin's `fetch_events` response should provide at runtime under the per-event `context` key. See [automation node plugin configuration](/plugins/automation-nodes/plugin-configuration#event-context) for the runtime side. *** ## Request format When a creator adds your block to an email or adjusts its settings, Kit makes a POST request to your `Request URL` with the following body: ```json theme={null} { "settings": { "button_text": "Complete your order", "background_color": "#ffffff", "title_color": "#000000" }, "styles": { "p": { "color": "#333333", "font-family": "Georgia, serif", "font-size": "16px" }, "h1": { "font-size": "2em", "font-weight": "bold" }, "h2": { "font-size": "1.5em", "font-weight": "bold" }, "a": { "color": "#0066cc" }, "ul": {}, "ol": {} }, "dynamic": true, "context_variable": "abandoned_checkout" } ``` When there is no linked automation event plugin, `context_variable` is omitted. * **`settings`** — The creator's current sidebar setting values, same as a standard content block. * **`styles`** — The surrounding email's typography styles, keyed by HTML tag (`p`, `h1`–`h6`, `a`, `ul`, `ol`, `blockquote`). Each value is an object of CSS property/value pairs (e.g. `color`, `font-family`, `font-size`, `line-height`). Use these to match your block's output to the email's design. Tags with no overrides are included as empty objects. * **`dynamic`** — Always `true` for dynamic content block requests. * **`context_variable`** — Present when the block is linked to an automation event plugin. Value is the **`identifier` string of that linked event node** (the same value that appears as the middle segment in Liquid: `automation.{app_identifier}.{event_node_identifier}.*`). Kit does **not** send merged workflow context or subscriber payloads on this POST—only `settings`, `styles`, `dynamic`, and optionally `context_variable`. Use `context_variable` if you need to branch server-side logic between event types; fetch live data from your own systems using `settings` and your app’s auth model. *** ## Response format Your endpoint should respond with a JSON object. Kit’s response schema **requires** an `html` key; other top-level keys are not validated out, but only `html` is required for success. The `html` value should be a Liquid template that references automation data using `automation.{app_identifier}.{event_node_identifier}.*` when linked to an event node: ```json theme={null} { "html": "{% if automation.yourapp.abandoned_checkout %}...{% endif %}" } ``` On error, return a non-2xx HTTP status. You may include an `errors` array in the body for debugging or logging: ```json theme={null} { "errors": ["No products found"] } ``` See [liquid templates](/plugins/content-blocks/dynamic-blocks/liquid-templates) for guidance on writing the HTML template, including available variables, loops, and email compatibility best practices. *** ## Authentication Dynamic content block plugins use the same OAuth flow as standard plugins. See [OAuth authorization](/plugins/oauth-authorization) for setup details. # Dynamic content blocks plugin flow Source: https://developers.kit.com/plugins/content-blocks/dynamic-blocks/plugin-flow Step-by-step guide to building a dynamic content block integration Let's say you're building an abandoned cart recovery integration. Subscribers who leave items in their cart are enrolled in a Visual Automation that sends them an email containing a personalized block showing exactly what they left behind. Here's the full flow:# Example content block plugin Source: https://developers.kit.com/plugins/content-blocks/example-plugin Simple example content block plugin You create an [automation node plugin](/plugins/automation-nodes/overview) that fires when a subscriber abandons their cart. This event plugin is responsible for capturing the subscriber's cart data and returning it on each event in the `context` field of your `fetch_events` response. Kit stores this per-subscriber context and exposes it in Liquid under a path like `automation.yourapp. .*`, where ` ` is the **`identifier` on that event node** (not the dynamic content block's identifier). You also configure the event's **Sample data** JSON on the event plugin form. The top-level keys define the fields creators can reference in Liquid, and the nested values are used to render previews in the email editor. See [automation node plugin configuration](/plugins/automation-nodes/plugin-configuration#event-context). You create a POST endpoint on your server that generates the Liquid-templated HTML for your content block. This endpoint receives the creator's settings choices and returns HTML containing Liquid variables—for example, `{{ automation.yourapp.abandoned_checkout.checkout_url }}` when the linked event node's `identifier` is `abandoned_checkout`. *Details on configuring this endpoint and the expected request/response format are in [plugin configuration](/plugins/content-blocks/dynamic-blocks/plugin-configuration).* If your block's sidebar includes a [search input](/plugins/component-library/search-input) to let creators browse or select data from your service (for example, choosing a fallback product to display), create a POST endpoint to serve those results. You [create your app in Kit](https://app.kit.com/apps?is=created) and configure both the automation event plugin and the dynamic content block plugin. On the event plugin, paste **Sample data** JSON into the Context card. On the content block plugin, set `dynamic` to `true` and link it to your event plugin using `related_plugin_id`—the read-only sample-data preview will appear automatically. Once both plugins are ready, submit your app for approval and publish. A Kit user installs your app, triggering your plugin's [OAuth authorization](/plugins/oauth-authorization) flow. The creator sets up a Visual Automation workflow that includes your automation event plugin (e.g., "Abandoned Cart"). When a subscriber triggers this event, Kit captures the data your plugin provides and stores it in the automation context for that subscriber. Inside the Visual Automation, the creator edits the sequence email and adds your dynamic content block from the element menu. Upon adding it—and each time they adjust a setting—Kit makes a POST request to your content block endpoint with the creator's settings. Your server responds with HTML containing Liquid variables that reference the automation context—for example: ```html theme={null} {% if automation.yourapp.abandoned_checkout %} {% for product in automation.yourapp.abandoned_checkout.products limit: 4 %} {% endfor %} Complete your order {% endif %} ``` Kit renders this template with your `default_values` to generate a preview for the creator to see in the editor.
{{ product.name }} {{ product.price }} When the automation sends the email to a subscriber, Kit renders the stored Liquid template using that subscriber's actual automation data—filling in their real cart contents, checkout URL, and totals. Each subscriber receives a personalized version of the block. Example OAuth authenticated content block plugin # Content blocks overview Source: https://developers.kit.com/plugins/content-blocks/overview Introducing content block plugins This functionality allows you to add your own elements to Kit's editor. The content of these elements are generated by endpoints on your server. You can submit your app to the Kit App Store for approval so your plugins can be used by other creators. To get started, first sign into our app and visit the [Build tab on the Kit App Store](https://app.kit.com/apps?is=created) to create your app, configure your plugin access, and create your plugins. Each plugin will appear as an item under your apps's name in our editor’s element menu. For more details on setting up apps and plugins, visit our [app](/kit-app-store/building-apps) and [plugin](/plugins/managing-plugins) building guides.# Content blocks plugin configuration Source: https://developers.kit.com/plugins/content-blocks/plugin-configuration Setting up your content block plugins Kit's content block plugins let you extend our email editor with custom HTML elements. This guide walks you through configuring your plugin's appearance, behavior, and settings—from naming and visual presentation to backend functionality and user controls.
**Standard vs. dynamic content blocks:** Standard content blocks return the same HTML to every recipient. If you need subscriber-personalized content—such as showing each recipient the items they left in their cart—see [dynamic content blocks](/plugins/content-blocks/dynamic-blocks/overview), which return a Liquid template that Kit renders individually at send time. ## Name The plugin `name` is the user-facing name for your block. In the content block insertion menu example above, “Post”, and “Product” are `names`. It will appear in the editor’s element menu, and also in the breadcrumbs at the top of the sidebar when your block is selected. Your `name` should be short, ideally one or two words. ## Description The `description` is a short phrase describing your block. It will appear underneath the name in the element menu. In the example above, “Add a link to a post” and “Add a link to a product”. ## Sort order If you offer multiple elements, the `sort order` determines their placement. In the example above, “Post” has a `sort order` of `0`, while “Product” has a `sort order` of `1`. ## Icon The `icon` is an image for the node to be displayed alongside the `name` and the `description`. A monochrome SVG is recommended. PNG, GIF, JPEG extensions are also supported. The recommended size is 150x120px. ## Request URL The `Request URL` is the URL of an endpoint on your server that returns an HTML string. This endpoint’s job is to generate the HTML for your element to be rendered in the email editor. You’ll generate this HTML based on the settings you’ve defined for your block (outlined in the [plugin settings page](/plugins/content-blocks/plugin-settings)). Once a user completes all required settings, we’ll make a POST request to your `Request URL`. The request will contain a `settings` object with the user’s selected values for each of your settings: ```json theme={null} { "settings": { // The exact data in this section depends on how you've configured your // plugin's JSON settings (see next section). "title": "My title", "description": "My description" } } ``` Your endpoint should return HTML for the element: ```html theme={null} ![]()
``` Your `Request URL` should respond to this request with a JSON object containing an `html` key: ```json theme={null} { "code": 200, "html": "{{ settings.title }}
{{ settings.description }}
...your HTML..." } ``` Or, if you've encountered an error, return an object containing an `errors` array of strings. You may add as many errors to this array as you’d like: ```json theme={null} { "code": 404, "errors": ["Plan not found"] } ``` ## Settings JSON This field allows you to configure the sidebar settings for your element. It should be an array of objects; one object for each setting. For instance, this would be the JSON configuration for a plugin with two settings: “Title” and “Color”. ```json theme={null} [ { "type": "text", "name": "title", "label": "Title", "placeholder": "Enter a title...", "required": true }, { "type": "color", "name": "title_color", "label": "Color", "required": true } ] ``` Each setting’s `type` determines the UI rendered (such as a text input or a color picker); all available options are listed under the [plugin settings page](/plugins/content-blocks/plugin-settings). The `name` for each setting is used as the key in your HTML request:When you save your plugin, we’ll validate your JSON settings - which can be pre-emptively validated [using the JSON schema validator linked here](https://www.jsonschemavalidator.net/s/ovDMo04X). # Content blocks plugin flow Source: https://developers.kit.com/plugins/content-blocks/plugin-flow Example flow for the content block plugin Let’s say you were developing an integration that allowed users to embed “Products” from your app inside the Kit editor. The full flow would look like this:
# Content blocks plugin recommendations Source: https://developers.kit.com/plugins/content-blocks/plugin-recommendations Best practices, tips and tricks to get the most out of content block plugins * For email client compatibility, we suggest that you use inline `style` attributes such as `` instead of CSS classes. * To test email client compatibility, we recommend using [https://www.caniemail.com](https://www.caniemail.com) and [https://www.litmus.com/](https://www.litmus.com/) * The only exception to this is if you need to include media queries to make your elements responsive on mobile, for example: ```html theme={null} You create a POST endpoint on your server to generate the HTML for your Product embed. *Details on configuring the `HMTL URL` [can be found here](/plugins/content-blocks/plugin-configuration#html-url).* *Optional* - You create a POST endpoint on your server to allow users to search for products. This is only necessary if you add a [search input](/plugins/content-blocks/plugin-settings#search-input), because we need to retrieve the results from somewhere. You [create your app within Kit](https://app.kit.com/apps?is=created) and configure the Products plugin. Once your app and plugin are ready, you submit your app for approval and publish. A Kit user installs your app, triggering and completing your plugin's OAuth authentication flow. The Kit user navigates to an email and searches for your Product plugin from the content block menu, or by using the `/Products` quick command. They select it from the menu to add it. Upon adding the plugin to the email, and upon all subsequent interactions with the plugin settings, Kit will make a request to the endpoint found in step 1 along with the values from the configuration settings selected by the creator - along with their access tokens to ensure authentication with your servers. You will respond to the request, returning HTML for the Product that adheres to all of the setting values selected by the creator - which we'll insert into our editor, ready to send. ...``` * Include `target=_blank` to all links * Use the full path URL for all links to ensure they can be opened * In most cases, you shouldn’t apply a background color to your plugin (unless the user is able to customize it). This is because the user might be using a custom background color on their email. By not applying a background color, your element’s background will automatically be the same as their email. # Plugin security Source: https://developers.kit.com/plugins/content-blocks/plugin-security Security for your content block plugins When we receive an HTML string from your server, we will [sanitize it](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html) to conform with recommended security practices. That means we’ll reject your HTML response if it includes any of the following: * Scripts, iframes * Audio, video elements * Form, input, command, action, prompt elements * External CSS styles, and CSS URLs # Content blocks plugin settings Source: https://developers.kit.com/plugins/content-blocks/plugin-settings Sidebar settings components This guide explains all of the sidebar settings components that are available to be used in your plugin's [settings JSON](/plugins/content-blocks/plugin-configuration#settings-json) configuration, giving your app users the ability to select and customize the HTML content they want to add into their email content.## Compatible settings components * [Color picker](/plugins/component-library/color-picker) * [Date picker](/plugins/component-library/date-picker) * [Font picker](/plugins/component-library/font-picker) * [Group](/plugins/component-library/group) * [Numerical input](/plugins/component-library/numerical-input) * [Radio group](/plugins/component-library/radio-group) * [Search input](/plugins/component-library/search-input) * [Select input](/plugins/component-library/select-input) * [Slider](/plugins/component-library/slider) * [Textarea](/plugins/component-library/textarea) * [Text input](/plugins/component-library/text-input) * [Toggle](/plugins/component-library/toggle) ## Refreshing data After a user has configured all required settings, we will perform a request to your server for the block's HTML. If the user wants to refresh the data, they can either: 1. Click the refresh button that appears when hovering over the element (pictured below), or 2. Change one of your plugin’s settings in the sidebar, which will automatically kick off another request for new HTML. ![]()
# Managing plugins Source: https://developers.kit.com/plugins/managing-plugins Creating and configuring your app's plugins This guide helps you through the process of managing your plugins, from starting out, to testing all the way through to release. This guide is split out to help guide you through a number of key concepts: * Creating your plugins * Configuring plugin authentication * Testing your plugins * Activating your plugins * Managing your existing plugins ## Configuring plugin authentication Before you can start creating plugins, you need to select and set up your authorization strategy for your plugins to let Kit know how to fetch the content needed to let creators use your apps. We currently offer 2 options:
If you require linking of third-party accounts or are sharing sensitive information, we currently only support OAuth authentication. This option will likely be the default for most applications, needing to pull data from a third-party account for the creator in a vast number of use cases. More details on OAuth authorization for plugins can be found in the [dedicated OAuth guide](/plugins/oauth-authorization). If you are working with public APIs, don’t need to pull any confidential information and are happy to have the content endpoints open, you can select “No authorization”. This can also be used temporarily to test our functionality before committing development time into building out an OAuth flow. If you try to create a plugin without setting your authentication strategy, you will be prompted to set this up, with 2 options; selecting: * "Continue without authentication" sets your strategy to "No authorization", or * "Configure Authentication" takes you to the Authentication tab to set this up
![]()
If you disable plugin authentication on the Authentication tab, you'll see a warning when you return to the Plugins tab. This warning indicates that you need to update authentication settings before your plugins can be active. Click the prompt to go directly to the Authentication tab and make the necessary changes. ## Create a new plugin Once you have selected your authentication method, you'll be able to create your first plugin! To do this, click on the "Plugins" tab in the sidebar menu when editing your app. When here, click on the "+ New plugin" button to start the process.![]()
If this is your second app, the "+ New plugin" button will be located to the right of the Plugin title: From here, a modal will appear asking for a name and plugin type. The name can be updated later and should be unique for the app you are creating. On overview for the available plugin types [can be found here](/plugins/overview), with more detail found in their dedicated sections in the Environments section below.After completing this step, you'll land on a dedicated setup page where you can configure your plugin settings.
## Testing your plugins So that new plugins can be tested for apps that have already been published, the developer account for an app is able to see all plugins set up for an app within their account *regardless of whether they are active or not*. This allows you to utilize your production system and your app authentication to test changes in a live system without worrying about other accounts accessing your plugins before they are ready. If you haven't yet gone live with your app and you want to envision what your plugins will look like for creators, you can start building out your plugins with public API endpoints and the "no authorization" authentication strategy, allowing you to tweak your content before finalising your OAuth authentication flows. ## Activating your plugin When you first create your plugin it will be inactive. While inactive it will only be available to test for your developer account, but once activated, the plugin will be visible to any user who has installed the app. ![]()
Details on the JSON required to set plugins up can be found in the plugin configuration documentation for the respective plugin environment. You must install your app from the Kit App Store to see your plugins in the editor. To do this, go to the ["Build" tab in the Kit App Store](https://app.kit.com/apps?is=created), click on "Preview" and install your app. In order to activate your plugin for Kit-wide usage, simply click the "Active" toggle and confirm that you want to set it live by clicking "Activate" in the resulting modal.activating-your-plugin At this point, the plugin will be available for all accounts that have installed your app. ## Managing your plugins Once you have created plugins within your app, you'll see your complete plugin list in the "Plugin" tab of your app, with options to: * Activate or deactivate plugins using toggles * Edit plugin settings
![]()
If a plugin is already active on a published app, we don’t recommend editing it until your changes have been thouroughly tested, as all updates to your plugins will take effect immediately, for accounts that have your app installed. Instead, create a new plugin and keep it inactive to test your changes. Once your tests are successful, you can update the original plugin with the new functionality. ### Deleting plugins You can also delete your unwanted plugins. In order to do this, simply edit the plugin you want to delete, and select the `Delete Plugin` button in the bottom left corner.Once you click this, a confirmation modal will appear, click `Delete` and the plugin will be deleted from the app.
![]()
This will stop creators from being able to add new instances of this plugin, but won't remove previously added instances to reduce the chance of broken content. This will allow creators to make the decision themselves on how they want to manage the plugin - by deleting it from their content or using the last valid state they saw. # Example media source plugin Source: https://developers.kit.com/plugins/media-source/example-plugin Simple example media source pluginExample media source plugin # Media source overview Source: https://developers.kit.com/plugins/media-source/overview Introducing media source plugins This functionality allows you to add your own images into Kit’s media gallery, letting creators search, filter and sort content from 3rd party sources right from the Kit email editor. To get started, first sign into our app and visit the [Build tab on Kit App Store](https://app.kit.com/apps?is=created) to create your App, configure your plugin access, and create your plugins. Each plugin will appear as an item under your company's name in our editor’s element menu as well as in the sidebar for the media gallery. For more details on setting up apps and plugins, visit our [app](/kit-app-store/building-apps) and [plugin](/plugins/managing-plugins) building guides. # Media source plugin configuration Source: https://developers.kit.com/plugins/media-source/plugin-configuration Setting up your media source plugins Kit's media source plugin lets you extend our email editor to allow creators to natively add images from external sources into their content. This guide walks you through configuring your plugin's appearance, behavior, and settings—from naming and visual presentation to backend functionality and user controls.## Name The plugin `name` is the user-facing name for your block and will be shown in the content block menu and in the media gallery itself. In the example above, the plugin `name` is “Your plugin name”. Your name should be short, ideally one or two words. ## Description The `description` is a short phrase describing your media source. It will appear underneath the name in the content block menu. In the example above this is set to “Short description for the plugin”. ## Sort order If you have multiple media sources within the same app, the `sort order` determines their placement within both the media gallery and the content block menu. In the images above, “Your plugin name” has a `sort order` of 0, while “Your plugin name 2” has a `sort order` of 1. ## Logo The `logo` is an image for the element to be displayed alongside the `name` and the `description`. Only PNG, GIF, JPEG extensions are supported. The recommended size is 150x120px. ## Request URL The `request URL` is the URL of an endpoint on your server that returns the list of images, complete with all the necessary properties to render the images in the gallery and place them within the email content for use by the creator. You’ll generate this response based on the settings you’ve defined for your media source (outlined in the next section). Once a user completes all required settings, we’ll make a POST request to your `request URL`. The request will contain a `settings` object with the user’s selected values for each of your search, filter, and sort settings, as well as pagination details, like so: ``` GET ![]()
![]()
? after=WzE0XQ==& settings[query]=Dogs& settings[label]=My Media& settings[sort]=updated_desc ``` *Note* - your app should also handle the empty case, ensuring you show some media by default when the creator opens up your plugin without interacting with any of the configuration elements your app offers. If you've encountered an error, return an object containing an errors array of strings. You may add as many errors to this array as you’d like. ```json theme={null} { "data": [], "errors": ["Plan not found"] } ``` ## Settings JSON The media gallery supports three optional groups that settings can be placed in: `search_group`, `filter_group`, & `sort_group`. Each group currently accepts a single setting that can be used by creators to filter and sort your content for ease of use. Configuration with a search, filter and sort component could look like so: ```json Example JSON [expandable] theme={null} [ { "type": "group", "name": "search_group", "label": "search", "settings": [ { "type": "text", "name": "search", "label": "Search", "help": "Search Giphy", "required": false } ] }, { "type": "group", "name": "filter_group", "label": "filter", "settings": [ { "type": "select", "label": "Rating", "name": "rating", "options": [ { "label": "G", "value": "g" }, { "label": "PG", "value": "pg" }, { "label": "PG-13", "value": "pg-13" } ], "required": false } ] }, { "type": "group", "name": "sort_group", "label": "sort", "settings": [ { "type": "select", "name": "sort", "label": "Sort", "options": [ { "label": "Alphabetical (A-Z)", "value": "alphabetical_asc" }, { "label": "Alphabetical (Z-A)", "value": "alphabetical_desc" } ] } ] } ] ``` Details on the individual configuration options can be found on the [plugin settings page](/plugins/media-source/plugin-settings). # Media source plugin flow Source: https://developers.kit.com/plugins/media-source/plugin-flow Example flow for the media source plugin Let’s say you were developing an integration that allowed users to embed design files from your app natively within the Kit editor. The full flow would look like this: A cursor for paginating forwards through the media items. A cursor for paginating backwards through the media items. A number to limit the amount of media items returned in the payload. A setting value entered. The name will be set to the nameconfigured for the plugin setting.Response Schema: *application/json* Pagination information for the payload Whether there is a previous page of media items to cycle back to. Whether there is a next page of media items to cycle through. A cursor identifying the first record of the returned media items that can be used with the `before` query param to fetch the previous page's items. If there is no previous page (on the first page) this should be null. A cursor identifying the last record of the returned media items that can be used with the `after` query param to fetch the next page's items. If there is no next page (on the last page) this should be null. At most how many records were limited to the payload. The list of media items for the page A unique identifier for the media. The type of media - only `image` is only supported at this moment. A link to the media item A link to the media item's thumbnail A textual description of the media item The default caption to be used to describe the media item. The creator will be able to overwrite this when selecting the media. A label for the creator to identify the media item. This is only shown to the creator for organizing their media items. Whether the media should always be directly used and embedded via the href. Setting to `true` will prevent the image from being reuploaded on Kit's servers. A URL to an endpoint that accepts can accept a POST request for notifying that the media was downloaded. Information about the media creator for attribution. A short label to use when the media is displayed to attribute the original creator A link to the original creator. ```json 200 response theme={null} { "pagination": { "has_previous_page": false, "has_next_page": true, "start_cursor": "WzEzXQ==", "end_cursor": "WzE0XQ==", "per_page": 100 }, "data": [ { "id": "example1", "type": "image", "url": "https://picsum.photos/600/900", "thumbnail_url": "https://picsum.photos/200/400", "alt": "Lorem ipsum odor amet, consectetuer adipiscing elit." }, { "id": "example2", "type": "image", "url": "https://picsum.photos/500/800", "thumbnail_url": "https://picsum.photos/200/400", "alt": "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." } ] } ``` ```json 200 response with additional properties theme={null} { "pagination": { "has_previous_page": false, "has_next_page": true, "start_cursor": "WzEzXQ==", "end_cursor": "WzE0XQ==", "per_page": 100 }, "data": [ { "id": "example1", "type": "image", "url": "https://picsum.photos/600/900", "thumbnail_url": "https://picsum.photos/600/900", "alt": "Lorem ipsum odor amet, consectetuer adipiscing elit.", "caption": "Lorem ipsum odor amet", "title": "example.png", "notify_download_url": "https://media-gallery-plugin.com/media/0/downloaded", "hotlink": true, "attribution": { "label": "Johnny Appleseed", "href": "https://example.com/johnny_appleseed?utm_source=your_app_name&utm_medium=referral" } } ] } ``` # Media source plugin recommendations Source: https://developers.kit.com/plugins/media-source/plugin-recommendations Best practices, tips and tricks to get the most out of media source plugins * For the best creator experience, ensure the resolution for the thumbnail is sufficient to best showcase the full image within the media gallery - this may mean using the full image asset rather than a traditional thumbnail * Alt text is important to ensure accessibility for all creators, regardless of how they use our service, making sure this is clear is integral for a high quality plugin * We recommend to only use hotlinks where absolutely necessary - such as if your image URL expires, or your platform prohibits the use of a copy of the image - to ensure the asset is always available for use # Media source plugin settings Source: https://developers.kit.com/plugins/media-source/plugin-settings Media gallery settings components This guide explains all of the media gallery settings components that are available to be used in your plugin's [settings JSON](/plugins/media-source/plugin-configuration#settings-json) configuration, giving your app users the ability to find the media they want to add into their email content. Create a POST endpoint on your server to generate the JSON to render your media in the media gallery (see [request URL](/plugins/media-source/plugin-configuration#request-url) for more details) *Optionally* * If using a dynamic filter (see [dynamic select input](/plugins/media-source/plugin-settings#filter) for more details), create a POST endpoint on your server to return the elements for the filter select menu * If you require notification for image insertion (see [request URL](/plugins/media-source/plugin-configuration#request-url)for more details), create a POST endpoint on your server that we will call when an image has been placed into an email You [create your app within Kit](https://app.kit.com/apps?is=created) and configure the designs plugin using [the guide found here](/plugins/media-source/plugin-configuration). Once your app and plugin are ready, you submit your app for approval and publish. A Kit user installs your app, triggering your plugin's OAuth authentication flow. The Kit user navigates to an email and searches for your designs plugin by: * Using the / command to search for the name of your plugin or app, to filter the content menu * Pressing the + button and scrolling down to your plugin, found under your app's heading * Using either method above to select *image* from the menu and navigating to the designs plugin found in the media gallery sidebar Upon opening your plugin in the media gallery, our system makes an initial request to the endpoint in #1 for the default list of images we should show from your server. If your settings include a dynamic select, we will also make a request to the endpoint in step 2 to render the select elements in the filter dropdown Any time the creator interacts with and fills out all required settings, our system makes a request to your endpoint from #1 with the values You will return the JSON for the media source, which we'll use to render the images in the media gallery for selection A creator scrolls through the list, triggering additional pagination requests from Kit A creator selects an image, whereby Kit inserts the image into the email content - if you have set up image insertion notifications from step 2, we will also notify you at this point ## Compatible settings components * [Dynamic select input](/plugins/component-library/dynamic-select-input) * [Select input](/plugins/component-library/select-input) * [Text input](/plugins/component-library/text-input) ![]()
Components for media source plugins work slightly differently than other plugin types. Check below to see how to utilize our preset `search`, `filter` and `sort` functionality, powered by `groups`: ### Search The optional search functionality utilizes a [text input component](/plugins/component-library/text-input), housed in a media source specific `group` called `search_group` - offering a way for creators to filter your images through a text filter. ```json theme={null} { "type": "group", "name": "search_group", "settings": [ { "type": "text", "name": "search", "label": "Search", "help": "Search Giphy", "required": false } ] } ``` It is good practice to make this as smart as possible - ensuring that you return the elements that make sense with the text inputted by the user. This means matching against the name of the image, but also any other relevant metadata a creator may be filtering by (e.g. you may want to include the name of the Folder within the search logic). Upon keystroke, a new POST request will be made to the `request URL` specified, with the text inputted, for your app to return a newly sorted version of results.### Filter The optional filter functionality allows plugins to offer a flat, single-select dropdown the creator can select from to help them find the content they are looking for. This can be predefined through use of the [select input component](/plugins/component-library/select-input) or programmatically generated as the plugin loads, through use of the [dynamic select input component](/plugins/component-library/dynamic-select-input). ![]()
Regardless of whether your plugin utilizes a [select input component](/plugins/component-library/select-input) or a [dynamic select input component](/plugins/component-library/dynamic-select-input), this will also need be housed in a media source specific `group`, named `filter_group`. Examples for each can be found below: ![]()
```json Select input theme={null} { "type": "group", "name": "filter_group", "settings": [ { "type": "select", "label": "Rating", "name": "rating", "options": [ { "label": "G", "value": "g" }, { "label": "PG", "value": "pg" }, { "label": "PG-13", "value": "pg-13" }, { "label": "R", "value": "r" } ], "required": false } ] } ``` ```json Dynamic select input theme={null} { "type": "group", "name": "filter_group", "settings": [ { "type": "dynamicSelect", "label": "Folders", "name": "folder", "request_url": "https://example-plugin.com/folders", "required": false } ] } ``` ### Sort The optional sort functionality utilizes a [select input component](/plugins/component-library/select-input) to offer a flat list of sort options the creator can select from, each with a `label` and `value` nested in an `options` array. Once an `option` is selected, a new POST request will be made to the *Request URL* specified, with the value selected, for your app to return a newly sorted version of results. This will need to be encased by a media source specific `group` named `sort_group`. ```json theme={null} { "type": "group", "name": "sort_group", "settings": [ { "type": "select", "name": "sort", "label": "Sort", "options": [ { "label": "Alphabetical (A-Z)", "value": "alphabetical_asc" }, { "label": "Alphabetical (Z-A)", "value": "alphabetical_desc" } ] } ] } ```# OAuth authorization Source: https://developers.kit.com/plugins/oauth-authorization Setting up OAuth authorization for your plugins The following guide helps guide you through the endpoints required for plugin authorization. For more details on setting up app authorization flows as a whole, check out our [app authentication guide here.](/kit-app-store/authentication) ## Plugin OAuth server setup For OAuth, you'll need to support 4 endpoints: * Getting an authorization code grant * Requesting an access token using an authorization code * Requesting an access token using a refresh token * Revoking an access token The endpoints must accept the requests outlined below and return responses minimally matching the outlined response shapes (additional attributes can be returned but we require at least what appears in these docs). The redirect URI we'll use for all of our requests will be [https://app.kit.com/apps/install](https://app.kit.com/apps/install). We'll use Bearer Authorization to include the user's access token on all the requests we make to your endpoints. ![]()
A diagram for this flow for apps that only require Plugin OAuth authentication can be found below. For guidance on apps that also require API authentication, also check out the [app authentication guide here.](/kit-app-store/authentication). ### Get an authorization code grant We will start the OAuth process by making a GET request to your provided authorization URL: ``` GET
? client_id= & response_type=code& redirect_uri=https://app.kit.com/apps/install ``` ### Get a new token using an authorization code We will then exchange the returned authorization code for a new access token by making a POST to your configured token URL, with a body like so: ```json theme={null} { "client_id": " Your plugin's configured Client ID `code` `https://app.kit.com/apps` Custom state to pass to the redirect\_uri and/or to protect from XSRF ```shell shell theme={null} curl -G GET \ -H 'Accept: text/html' -d "client_id= " -d "response_type=code" -d "redirect_uri=https://app.kit.com/apps/install" ``` ```javascript Javascript theme={null} const headers = { 'Accept':'text/html' }; fetch(' ?client_id= &response_type=code&redirect_uri=https://app.kit.com/apps/install', { method: 'GET', headers: headers }) .then(function(res) { return res.json(); }).then(function(body) { console.log(body); }); ``` ```ruby Ruby theme={null} require 'rest-client' require 'json' headers = { 'Accept' => 'text/html' } result = RestClient.get ' ?client_id= &response_type=code&redirect_uri=https://app.kit.com/apps/install', params: { }, headers: headers p JSON.parse(result) ``` ```python Python theme={null} import requests headers = { 'Accept': 'text/html' } r = requests.get(' ?client_id= &response_type=code&redirect_uri=https://app.kit.com/apps/install', headers = headers) print(r.json()) ``` **302:** Redirects to `https://app.kit.com/apps` with authorization code parameter ", "client_secret": " ", "grant_type": "authorization_code", "code": "abc123", "redirect_uri": "https://app.kit.com/apps" } ``` ### Get a new token using a refresh token When the previous access token expires, we will request a new access token by making a POST to your configured refresh token URL, with a body like so: ```json theme={null} { "client_id": " Response schema: *application/json* Your plugin's configured Client ID Your plugin's configured Client Secret `authorization_code` The code received via the redirect uri query params `https://app.kit.com/apps` ```shell shell theme={null} curl -X POST \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Accept: application/json' \ -d '{ "client_id": " ", "client_secret": " ", "grant_type": "authorization_code", "code": "abc123", "redirect_uri": "https://app.kit.com/apps" } ``` ```javascript Javascript theme={null} const inputBody = '{ "client_id": " ", "client_secret": " ", "grant_type": "authorization_code", "code": "abc123", "redirect_uri": "https://app.kit.com/apps" }'; const headers = { 'Content-Type':'application/x-www-form-urlencoded', 'Accept':'application/json' }; fetch(' ', { method: 'POST', body: inputBody, headers: headers }) .then(function(res) { return res.json(); }).then(function(body) { console.log(body); }); ``` ```ruby Ruby theme={null} require 'rest-client' require 'json' headers = { 'Content-Type' => 'application/x-www-form-urlencoded', 'Accept' => 'application/json' } payload = { "client_id" => " ", "client_secret" => " ", "grant_type" => "authorization_code", "code" => "abc123", "redirect_uri" => "https://app.kit.com/apps" } result = RestClient.post ' ', payload, params: { }, headers: headers p JSON.parse(result) ``` ```python Python theme={null} import requests headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' } json = { "client_id": " ", "client_secret": " ", "grant_type": "authorization_code", "code": "abc123", "redirect_uri": "https://app.kit.com/apps" } r = requests.post(' ', headers = headers, json = json) print(r.json()) ``` **200:** Returns a token
```json theme={null} { "access_token": "YOUR_ACCESS_TOKEN_HERE", "expires_in": 172800, "refresh_token": "YOUR_REFRESH_TOKEN_HERE", "created_at": 1710270147 } ```Response schema: *application/json*
Access token for a user in the plugin app's system When the access token expire in seconds Refresh token that can be used to generate a new access token once this one expires When the access token was created ", "grant_type": "refresh_token", "refresh_token": "string" } ``` ### Revoke an access token When your app is uninstalled by a creator, we will make a POST request to your revoke token URL, with a body like so: ```json theme={null} { "client_id": " Response body schema: *application/json* Your plugin's configured Client ID `refresh_token` `Refresh token` ```shell shell theme={null} curl -X POST \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Accept: application/json' \ -d '{ "client_id": " ", "grant_type": "refresh_token", "refresh_token": "abc123" }' ``` ```javascript Javascript theme={null} const inputBody = '{ "client_id": " ", "grant_type": "refresh_token", "refresh_token": "string" }'; const headers = { 'Content-Type':'application/x-www-form-urlencoded', 'Accept':'application/json' }; fetch(' ', { method: 'POST', body: inputBody, headers: headers }) .then(function(res) { return res.json(); }).then(function(body) { console.log(body); }); ``` ```ruby Ruby theme={null} require 'rest-client' require 'json' headers = { 'Content-Type' => 'application/x-www-form-urlencoded', 'Accept' => 'application/json' } payload = { "client_id" => " ", "grant_type" => "refresh_token", "refresh_token" => "abc123" } result = RestClient.post ' ', payload, params: { }, headers: headers p JSON.parse(result) ``` ```python Python theme={null} import requests headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' } json = { "client_id": " ", "grant_type": "refresh_token", "refresh_token": "abc123" } r = requests.post(' ', headers = headers, json = json) print(r.json()) ``` **200:** Returns a token
```json theme={null} { "access_token": "YOUR_NEW_ACCESS_TOKEN_HERE", "expires_in": 7200, "refresh_token": "YOUR_NEW_REFRESH_TOKEN_HERE", "created_at": 1710271006 } ```Response schema: *application/json*
Access token for a user in the plugin app's system When the access token expire in seconds Refresh token that can be used to generate a new access token once this one expires When the access token was created ", "client_secret": " ", "token": "abc123" } ``` ## App configuration for OAuth To set OAuth up for your app, go to the "Authentication" tab on your app, toggle on the "Plugin" section and select "OAuth" from the "Authorization method" dropdown: Response body schema: *application/json* Your plugin's configured Client ID Your plugin's configured Client Secret The access token ```shell shell theme={null} curl -X POST \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Accept: application/json' \ -d '{ "client_id": " ", "client_secret": " ", "token": "abc123" }' ``` ```javascript Javascript theme={null} const inputBody = '{ "client_id": " ", "client_secret": " ", "token": "string" }'; const headers = { 'Content-Type':'application/x-www-form-urlencoded', 'Accept':'application/json' }; fetch(' ', { method: 'POST', body: inputBody, headers: headers }) .then(function(res) { return res.json(); }).then(function(body) { console.log(body); }); ``` ```ruby Ruby theme={null} require 'rest-client' require 'json' headers = { 'Content-Type' => 'application/x-www-form-urlencoded', 'Accept' => 'application/json' } payload = { "client_id" => " ", "client_secret" => " ", "token" => "abc123" } result = RestClient.post ' ', payload, params: { }, headers: headers p JSON.parse(result) ``` ```python Python theme={null} import requests headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' } json = { "client_id": " ", "client_secret": " ", "token": "abc123", } r = requests.post(' ', headers = headers, json = json) print(r.json()) ``` **200:** OK This will expand the section and offer the fields to add your: * Authorization URL * Token URL * Refresh token URL * Revoke URL as well as the "Client ID" and "Client secret" fields for us to authenticate with your service:
Once all of the fields are filled out, click save and OAuth will be set up for all plugins you create for the app. ### Post-installation redirect Your app may also include the option to alternatively send creators to your app, or an externally hosted onboarding flow, post signup. This can be configured using the `Redirect URL after install` field in your [app details setting page](/kit-app-store/app-details-page). An example of this flow can be seen below.
### Plugin scopes When configuring OAuth for your plugins, you're able to define scopes that specify which permissions your app requires from the creator's account. These scopes are managed in the plugin settings page and play a crucial role in app versioning and authentication. ##### Configuring Scopes In the plugin settings page, you can define the scopes your plugin requires. Kit tracks these scopes cumulatively across all plugins in your app, which means: * Each plugin can declare its own required scopes * The total scopes for your app are the combined unique scopes from all active plugins * Changes to cumulative scopes trigger app versioning ##### Scope Format Requirements When defining scopes, follow these critical formatting rules: * **Never include spaces in scope names** - Use underscores, hyphens, camel/snake case or colons instead * ❌ Incorrect: `"read data"`, `"write data"` * ✅ Correct: `"read:data"`, `"write:data"`, `"read_data"` * Spaces cause serialization errors when scopes are processed as arrays or comma-separated strings * Use consistent naming conventions across all your plugins (recommended format: `action:resource`) ##### When Scopes Trigger Re-authentication Creators will be prompted to re-authenticate when: * You add a new scope to any plugin that wasn't previously used by other plugins * You create a new plugin with scopes not used by existing plugins * The authorization strategy changes (for example, from "No authorization" to "OAuth") Removing scopes does not require re-authentication, making it safe to reduce permissions without disrupting users. ##### Testing Scope Changes When developing and testing scope changes: * Plugins in test mode (created and have never been activated) require you to re-authenticate the app in your developer account when new scopes are added * This allows you to verify the authentication flow before affecting production users * Always test the complete OAuth flow with new scopes before publishing ##### Example If your app has two plugins with the following scopes: * **Product Gallery Plugin**: `read:products`, `read:inventory` * **Customer Reviews Plugin**: `read:products`, `read:reviews` Your app's cumulative scopes are: `read:products`, `read:inventory`, `read:reviews` Adding `write:products` to either plugin would trigger a new app version and require re-authentication, while adding `read:products` to a new third plugin would not. For comprehensive information about app versioning and its implications for both creators and developers, see the [App Versioning Guide](/kit-app-store/app-versioning). # Plugin overview Source: https://developers.kit.com/plugins/overview Kit plugins integrate seamlessly with our creators' workflows, giving them instant access to third-party content and creative assets directly from within the Kit UI. Unlike basic integrations creators have to manually stitch together, plugins become part of their Kit experience—transforming their email platform into a personalized hub that streamlines their creative business, powered by the apps and plugins you create. Unlike API based apps, plugins that connect to third-party tools authenticate the other way around, with Kit needing to authenticate against your third-party service to be able to pull the data required to be rendered within Kit. More details on setting up authentication can be found in our [plugin OAuth guide](/plugins/oauth-authorization). ## Plugin types We currently offer three plugin types: * [Content blocks](/plugins/content-blocks/overview) * [Media source](/plugins/media-source/overview) * [Automation nodes](/plugins/automation-nodes/overview) ### Content blocks Content blocks let you create dynamic HTML elements for creators to directly insert into your emails. Help creators promote their upcoming events, sell their latest product, or showcase their latest podcast episode—all without leaving the Kit editor. These blocks make creators' emails more engaging while saving them valuable creative time from going back-and-forth between tools. ### Media sources Media sources let creators connect to and filter external media galleries right from the Kit media gallery—bringing their content to where they need it, when they need it. This offers simple filtration and search functionality, helping you get creators to the content they want, as quickly as possible. ### Automation nodes Automation nodes let you extend Kit's visual automation tools with custom functionality, enabling creators to build powerful, automated workflows that connect your app with their subscriber journeys. Create Event nodes that trigger automations when specific conditions are met in your platform, or Action nodes that automatically perform tasks in your app when subscribers reach them in a funnel. These nodes help creators save time by automating repetitive tasks and create sophisticated subscriber experiences that span multiple tools – all within Kit's intuitive Visual Automation builder. # Delivery format Source: https://developers.kit.com/webhooks/delivery-format The envelope, headers, and HTTP behavior of a webhook delivery Every delivery is an HTTP `POST` to your endpoint's URL with a JSON body (the "envelope") and a set of headers. ## Envelope A delivery carries an **array of events**. Most deliveries carry exactly one; when many events of the same type happen at once (a bulk action in the app), Kit batches them into deliveries of up to 100 events each, so your handler should always iterate `events`. ```json theme={null} { "delivery_id": 123456, "events": [ { "id": "9c2e1f3a-6b7d-4e8f-a1b2-c3d4e5f60718", "type": "subscriber.created", "created": "2026-07-29T14:32:10Z", "data": { "subscriber": { "id": 987654, "first_name": "Ada", "email_address": "ada@example.com", "state": "active", "created_at": "2026-07-29T14:32:10Z", "fields": { "company": "Kit" } } } } ] } ``` | Field | Type | Description | | ------------- | ------- | ---------------------------------------------------------------------------------- | | `delivery_id` | integer | Identifies this POST. A [retry](/webhooks/retries) of a failed delivery reuses it. | | `events` | array | 1 to 100 events, all of the same `type`. | Each event in `events`: | Field | Type | Description | | --------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string (UUID) | Unique id of the event. **This is your [deduplication](/webhooks/verifying-signatures#idempotency) key** — it stays the same if the event is ever re-sent. | | `type` | string | The [event type](/webhooks/event-types). | | `created` | string | ISO 8601 UTC timestamp of when the event occurred (not when it was delivered). | | `data` | object | The event payload. Its shape depends on `type` — see [event types](/webhooks/event-types). | ![]()
![]()
A delivery never mixes event types and never carries more than 100 events. Kit also splits a batch once it would pass roughly 512 KB of event payload, so most deliveries stay well under that; a single event larger than 512 KB is still delivered on its own rather than dropped. ## Headers | Header | Description | | ----------------- | -------------------------------------------------------------------------------------------------------- | | `Content-Type` | Always `application/json`. | | `X-Kit-Delivery` | The `delivery_id`, matching the body. | | `X-Kit-Signature` | Timestamped HMAC signature of the raw body — see [Verifying signatures](/webhooks/verifying-signatures). | | `User-Agent` | `Kit-Webhooks/2.0`. | ## HTTP behavior * Deliveries are always `POST` requests. * Respond with any `2xx` status to acknowledge. Kit reads only the status code, not the response body. * Any non-`2xx` response, a connection failure, or a timeout marks the delivery failed and schedules a [retry](/webhooks/retries). A retry re-POSTs the **entire** `events` array, so process each event idempotently by its `id`. * Sign or validate against the **raw request body** exactly as received — don't re-serialize the JSON first, or the signature won't match.Your endpoint must be a public HTTP(S) URL. Kit blocks delivery to private, internal, or loopback addresses. # Errors and gating Source: https://developers.kit.com/webhooks/errors Error responses you may hit when managing webhook endpoints These are the errors you may encounter on the webhook **management** endpoints (`/v4/webhook_endpoints`). For the general API error model, see [response codes](/api-reference/response-codes). ## `422` Unprocessable — validation Returned when the request body is invalid. Common cases: * `events` is empty — an endpoint must subscribe to at least one event type. * `url` is missing or not a public HTTP(S) URL. * `description` is too long. ```json theme={null} { "errors": ["events must include at least one event type"] } ``` ## `403` Forbidden — app-managed endpoints Endpoints created by an app (via OAuth) are owned by that app. An **API key** cannot update or delete an app-managed endpoint — that returns `403`. (An app reaching for an endpoint it didn't create is scoped out entirely and gets a `404`, covered below.) ## `401` Unauthorized — plan access If the account's plan doesn't include API/webhook access (for example a free plan or a lapsed trial), the endpoints return `401`. See [response codes](/api-reference/response-codes) for the full list of `401` causes. ## `404` Not found — access gating The webhook endpoints return `404` (rather than revealing existence) when: * Webhooks aren't enabled for the account (the feature is off). * The endpoint id belongs to another account, or to an app other than the caller. ## Duplicate URLs are allowed Pointing more than one endpoint at the same URL is **not** an error — for example, one endpoint per event set, or rotating an endpoint by standing up a replacement before removing the old one. If two endpoints on the same URL subscribe to overlapping events, that URL receives one delivery per endpoint carrying the same events (same event `id`s); [deduplicate](/webhooks/verifying-signatures#idempotency) on the event `id`. # Event types Source: https://developers.kit.com/webhooks/event-types The events you can subscribe a webhook endpoint to Subscribe an endpoint to one or more event types with the `events` array when you [create or update it](/webhooks/getting-started). Each event arrives inside a delivery's [`events` array](/webhooks/delivery-format), and its `data` shape is described below.Kit is rolling event types out incrementally. The events marked **Available** below deliver today; the rest are planned and listed so you can see what's coming. Planned events can't be subscribed to yet: the API rejects them with a `422` and the create panel in Kit doesn't offer them. They become subscribable the day they ship. ## Subscriber events | Event | Status | | --------------------------------------- | ------------- | | `subscriber.created` | **Available** | | `subscriber.activated` | **Available** | | `subscriber.unsubscribed` | **Available** | | `subscriber.bounced` | **Available** | | `subscriber.complained` | **Available** | | `subscriber.subscribed_to_form` | **Available** | | `subscriber.added_to_sequence` | **Available** | | `subscriber.sequence_completed` | **Available** | | `subscriber.tag_added` | **Available** | | `subscriber.tag_removed` | **Available** | | `subscriber.custom_field_value_updated` | **Available** | | `subscriber.product_purchased` | Planned | | `subscriber.link_clicked` | Planned | | `subscriber.email_opened` | Planned | ## Resource events | Event | Status | | --------------------------------------------------------------------------------- | ------------- | | `tag.created`, `tag.deleted` | **Available** | | `custom_field.created`, `custom_field.deleted` | **Available** | | `sequence.created`, `sequence.deleted`, `sequence.published`, `sequence.disabled` | **Available** | | `broadcast.created`, `broadcast.sent`, `broadcast.deleted` | **Available** | | `post.published` | **Available** | | `landing_page.created`, `landing_page.deleted` | Planned | ## Payloads Subscriber events carry the `subscriber` object, plus the related resource where one exists. The `subscriber` object matches the shape returned by the [V4 subscribers API](/api-reference/overview): ```json theme={null} { "subscriber": { "id": 987654, "first_name": "Ada", "email_address": "ada@example.com", "state": "active", "created_at": "2026-07-29T14:32:10Z", "fields": { "company": "Kit" } } } ``` ### `subscriber.created` Fires when a subscriber is added to the account. `data` carries the `subscriber` object above. ### `subscriber.activated` Fires when a subscriber transitions into the `active` state. `data` carries the `subscriber` object above. ### `subscriber.unsubscribed`, `subscriber.bounced`, `subscriber.complained` Fire when a subscriber unsubscribes, when their address hard-bounces, and when they report an email as spam. Each carries the `subscriber` object above, whose `state` reflects the new status. ### `subscriber.subscribed_to_form` Fires when a subscriber joins a form. Includes both the subscriber and the form. ```json theme={null} { "subscriber": { "...": "..." }, "form": { "id": 55, "name": "Newsletter signup", "created_at": "2026-05-01T08:15:00Z", "type": "embed", "format": "inline", "archived": false, "uid": "abc123" } } ``` ### `subscriber.added_to_sequence` and `subscriber.sequence_completed` Fire when a subscriber starts a sequence, and when they finish its last email. Both include the sequence. ```json theme={null} { "subscriber": { "...": "..." }, "sequence": { "id": 77, "name": "Welcome series", "hold": false, "repeat": false, "created_at": "2026-05-01T08:15:00Z", "updated_at": "2026-07-01T10:00:00Z", "email_template_id": 3, "active": true } } ``` ### `subscriber.tag_added` and `subscriber.tag_removed` Fire when a tag is added to or removed from a subscriber. Both include the tag. Deleting a tag does **not** fire `subscriber.tag_removed` for each subscriber that had it — you get a single `tag.deleted` event instead (see below). ```json theme={null} { "subscriber": { "...": "..." }, "tag": { "id": 42, "name": "customer", "created_at": "2026-05-01T08:15:00Z" } } ``` ### `subscriber.custom_field_value_updated` Fires when a custom field value changes on a subscriber — one event per field changed. Includes the field and its new value. ```json theme={null} { "subscriber": { "...": "..." }, "custom_field": { "id": 9, "name": "ck_field_9_company", "key": "company", "label": "Company", "value": "Kit" } } ``` ### `sequence.created`, `sequence.deleted`, `sequence.published`, `sequence.disabled` Fire on the sequence itself, not on a subscriber. `data` carries the `sequence` object shown above. ### `tag.created` and `tag.deleted` Fire when a tag is created or deleted on the account. These are resource events — `data` carries the tag itself, no subscriber. ```json theme={null} { "tag": { "id": 42, "name": "customer", "created_at": "2026-05-01T08:15:00Z" } } ```Deleting a tag removes it from every subscriber at once, but Kit emits only the one `tag.deleted` event — there is no `subscriber.tag_removed` per subscriber. If you mirror tag membership, drop the tag from all subscribers when you receive `tag.deleted`. ### `broadcast.created`, `broadcast.sent`, `broadcast.deleted` Fire when a broadcast is created, when its send completes, and when it's deleted. `data` carries a summary of the broadcast; fetch the full representation, including content, from the [V4 broadcasts API](/api-reference/overview). ```json theme={null} { "broadcast": { "id": 5001, "publication_id": 12, "created_at": "2026-07-28T09:00:00Z", "subject": "This week's update", "preview_text": "What's new at Kit", "description": "Weekly newsletter", "public": true, "published_at": "2026-07-29T14:00:00Z", "send_at": "2026-07-29T14:00:00Z", "status": "completed" } } ``` ### `post.published` Fires when a post is published. `data` carries a summary of the post; fetch the full content from the [V4 posts API](/api-reference/overview). ```json theme={null} { "post": { "id": 8801, "publication_id": 12, "title": "Hello, world", "slug": "hello-world", "published_at": "2026-07-29T14:00:00Z", "is_paid": false, "public_url": "https://example.kit.com/posts/hello-world" } } ``` ### `custom_field.created` and `custom_field.deleted` Fire when a custom field is created or deleted on the account. These are resource events — `data` carries the field itself, no subscriber. ```json theme={null} { "custom_field": { "id": 9, "name": "ck_field_9_company", "key": "company", "label": "Company" } } ``` # Getting started with webhooks Source: https://developers.kit.com/webhooks/getting-started Create an endpoint, subscribe to events, and receive your first delivery This guide takes you from zero to a verified webhook delivery. You'll create an endpoint, subscribe it to an event, and confirm you can receive and verify a delivery. ## Prerequisites * A Kit account on a [plan with API access](https://kit.com/pricing). * An [API key or OAuth access token](/api-reference/authentication). Both work for managing endpoints; OAuth-created endpoints are scoped to the app that created them. * A publicly reachable HTTP(S) URL that can accept `POST` requests (HTTPS strongly recommended). For local development, a tunnel such as ngrok works well.No receiver yet? Create a temporary URL at [webhook.site](https://webhook.site) and register that as the endpoint. It shows every delivery Kit sends, headers and body included, so you can trigger a real event and inspect the payload before writing any code. Swap in your own URL when the receiver is ready. Register your URL and the events it should receive. The response includes a `secret` — **this is the only time it's returned in plaintext**, so store it now; you'll need it to [verify signatures](/webhooks/verifying-signatures). ```bash theme={null} curl -X POST https://api.kit.com/v4/webhook_endpoints \ -H "X-Kit-Api-Key: " \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/kit/webhooks", "events": ["subscriber.created"] }' ``` ```json theme={null} { "webhook_endpoint": { "id": 42, "name": "Webhook 1", "url": "https://example.com/kit/webhooks", "events": ["subscriber.created"], "status": "active", "source": "creator", "description": null, "created_by_app": null, "created_at": "2026-07-29T14:30:00Z", "previous_secret_expires_at": null, "secret": "whsec_9f8a...store-me" } } ``` Update the endpoint's event set at any time. The `events` array replaces the current subscriptions. ```bash theme={null} curl -X PATCH https://api.kit.com/v4/webhook_endpoints/42 \ -H "X-Kit-Api-Key: " \ -H "Content-Type: application/json" \ -d '{ "events": ["subscriber.created", "subscriber.activated"] }' ``` Browse everything you can subscribe to in the [event types catalog](/webhooks/event-types). Trigger the event (for `subscriber.created`, add a subscriber to the account). Kit sends a `POST` to your URL with the [envelope](/webhooks/delivery-format): ```json theme={null} { "delivery_id": 123456, "events": [ { "id": "9c2e1f3a-6b7d-4e8f-a1b2-c3d4e5f60718", "type": "subscriber.created", "created": "2026-07-29T14:32:10Z", "data": { "subscriber": { "id": 987654, "email_address": "ada@example.com", "...": "..." } } } ] } ``` `events` is always an array — usually one event, but bulk activity batches up to 100 per delivery, so iterate it rather than reading a single event. Respond with any `2xx` status to acknowledge receipt. Non-`2xx` responses (or timeouts) are [retried](/webhooks/retries). Every delivery carries an `X-Kit-Signature` header. Recompute the HMAC with your stored secret and compare before trusting the payload — see [Verifying signatures](/webhooks/verifying-signatures) for ready-to-paste code. Return `2xx` within 10 seconds and do slow work asynchronously. Kit waits 5 seconds to connect and 10 seconds for your response; past that the delivery counts as failed and gets [retried](/webhooks/retries), even if your handler eventually finished the work. # Webhooks overview Source: https://developers.kit.com/webhooks/overview Receive real-time HTTP notifications when things happen in a Kit account Webhooks let your integration react to activity in a Kit account the moment it happens, instead of polling the API. When an event occurs — a subscriber is created, activated, or joins a form — Kit sends an HTTP `POST` to a URL you control, with a JSON payload describing what happened. ## Core concepts * **Endpoint** — a URL you register to receive deliveries, plus the set of events it listens to. Create and manage endpoints through the [V4 API](/api-reference/overview) (`/v4/webhook_endpoints`) or in Kit at [app.kit.com/webhooks](https://app.kit.com/webhooks). * **Subscription** — the link between an endpoint and an [event type](/webhooks/event-types). One endpoint can subscribe to many events. * **Event** — something that happened in the account (for example `subscriber.created`). Every event has a UUID `id` you can use to [deduplicate](/webhooks/verifying-signatures#idempotency). * **Delivery** — one `POST` to one endpoint, carrying an array of 1 to 100 events of the same type. Most deliveries carry a single event; bulk activity batches into fewer, larger deliveries. ## What a delivery looks like Every delivery is a `POST` with a JSON [envelope](/webhooks/delivery-format) and a signature you can verify:```json theme={null} { "delivery_id": 123456, "events": [ { "id": "9c2e1f3a-6b7d-4e8f-a1b2-c3d4e5f60718", "type": "subscriber.created", "created": "2026-07-29T14:32:10Z", "data": { "subscriber": { "id": 987654, "first_name": "Ada", "email_address": "ada@example.com", "state": "active", "created_at": "2026-07-29T14:32:10Z", "fields": {} } } } ] } ``` ## Webhook endpoints supersede the legacy webhooks API The V4 API also has an older webhooks resource, [`/v4/webhooks`](/api-reference/webhooks-legacy/list-webhooks). Webhook endpoints supersede it: build anything new on `/v4/webhook_endpoints`, and expect new capabilities to land here only. Superseded doesn't mean removed. If you already deliver through `/v4/webhooks`, your integration keeps working — legacy webhooks aren't going anywhere, and there's nothing you have to migrate. The two systems run side by side, independently. What you gain by building on webhook endpoints: | | Legacy webhooks | Webhook endpoints (this guide) | | ---------------------- | ------------------------------------------------- | -------------------------------------------------------------------------------------- | | Create with | `/v4/webhooks` | `/v4/webhook_endpoints` | | Events per webhook | one | as many [event types](/webhooks/event-types) as you like, per endpoint | | Event catalog | a fixed set | a growing catalog of subscriber and resource events | | Delivery payload | a single event | a signed [envelope](/webhooks/delivery-format) batching up to 100 events | | Signature verification | — | [`X-Kit-Signature`](/webhooks/verifying-signatures), HMAC-SHA256 | | Failed deliveries | dropped | [retried — 8 attempts over \~41 hours](/webhooks/retries) | | Secret rotation | — | [live rotation with an overlap window](/webhooks/verifying-signatures#secret-rotation) | | Deduplication | — | UUID event `id`s, stable across re-sends | | Manage in the app | mixed into the Rules list; disable or delete only | a dedicated [app.kit.com/webhooks](https://app.kit.com/webhooks) page | Use webhook endpoints for every new integration. Existing legacy webhooks can stay exactly as they are — migrate whenever the capabilities above are worth having. ## Next steps# Retries Source: https://developers.kit.com/webhooks/retries How Kit retries webhook deliveries that fail A delivery is considered successful when your endpoint returns any `2xx` status. Anything else — a non-`2xx` response, a connection failure, or a timeout — marks the delivery failed and schedules a retry. ## Retry schedule Failed deliveries are retried with backoff: `30s`, `2m`, `15m`, `1h`, `4h`, `12h`, then `24h` — up to 8 attempts (the original plus 7 retries) over about 41 hours. After the last attempt the delivery is marked permanently failed. A retry re-POSTs the delivery's **entire** [`events` array](/webhooks/delivery-format) under the same `delivery_id`, re-signed at send time — so a late retry carries a fresh signature timestamp and whichever secrets are valid at that moment. ## Special cases * **`410 Gone`** — Kit stops retrying immediately and marks the delivery failed. Return `410` when an endpoint no longer exists so Kit doesn't keep trying. * **`429` / `503` with `Retry-After`** — Kit honors the `Retry-After` header when deciding when to try again (clamped between 1 second and 24 hours), so you can slow Kit down without failing outright. ## Designing for retries * **Be idempotent.** Your handler may see the same event more than once — a retried delivery repeats every event in it, and an upstream re-emit can carry the same event under a new delivery. Deduplicate on each event's UUID `id`, which stays the same across re-sends — see [idempotency](/webhooks/verifying-signatures#idempotency). * **Acknowledge fast.** Return `2xx` as soon as you've safely received the payload and do heavier work asynchronously, so a slow handler doesn't time out and trigger an avoidable retry. # Verifying signatures Source: https://developers.kit.com/webhooks/verifying-signatures Confirm a delivery genuinely came from Kit and wasn't tampered with Every delivery is signed with your endpoint's secret so you can confirm it came from Kit and the body wasn't altered in transit. Always verify before acting on a payload. ## The signature Kit sends an `X-Kit-Signature` header with a timestamp and one or more signatures, comma-separated: ``` X-Kit-Signature: t=1753797130,v1=8f4b1c...e2 ``` * `t` — Unix timestamp (seconds) of when this POST was signed. * `v1` — HMAC-SHA256 of the string `"{t}.{raw_body}"` (the timestamp, a literal `.`, then the raw request body), keyed with your endpoint's secret and hex-encoded. During [secret rotation](#secret-rotation) the header carries one `v1` entry per valid secret. The timestamp is part of the signed string, so an attacker can't replay an old body with a fresh timestamp. The secret is returned in plaintext only when you [create the endpoint](/webhooks/getting-started) or [rotate the secret](#secret-rotation) — store it securely; Kit never returns it again. ## Verifying Parse the header, rebuild the signed string from `t` and the exact bytes you received, compute the HMAC, and compare it against every `v1` entry using a constant-time comparison. Reject stale timestamps to guard against replays — 5 minutes is a sensible tolerance ([retries](/webhooks/retries) are re-signed at send time, so a legitimate late retry still carries a fresh `t`). Create your first endpoint and receive a delivery end to end. Confirm each delivery really came from Kit. Browse the events you can subscribe to. The envelope, headers, and HTTP behavior. ```ruby Ruby theme={null} require "openssl" TOLERANCE = 300 # seconds def valid_signature?(raw_body, header, secret) parts = header.to_s.split(",").map(&:strip) timestamp = parts.find { |p| p.start_with?("t=") }&.delete_prefix("t=") return false if timestamp.nil? || (Time.now.to_i - timestamp.to_i).abs > TOLERANCE expected = OpenSSL::HMAC.hexdigest("sha256", secret, "#{timestamp}.#{raw_body}") parts.select { |p| p.start_with?("v1=") }.any? do |candidate| ActiveSupport::SecurityUtils.secure_compare(candidate.delete_prefix("v1="), expected) end end ``` ```javascript Node.js theme={null} const crypto = require("crypto"); const TOLERANCE = 300; // seconds function validSignature(rawBody, header, secret) { const parts = (header || "").split(",").map((s) => s.trim()); const timestamp = parts.find((p) => p.startsWith("t="))?.slice(2); if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE) { return false; } const expected = crypto .createHmac("sha256", secret) .update(`${timestamp}.${rawBody}`) .digest("hex"); const expectedBuf = Buffer.from(expected); return parts .filter((p) => p.startsWith("v1=")) .some((candidate) => { const candidateBuf = Buffer.from(candidate.slice(3)); return ( candidateBuf.length === expectedBuf.length && crypto.timingSafeEqual(candidateBuf, expectedBuf) ); }); } ``` ```python Python theme={null} import hmac, hashlib, time TOLERANCE = 300 # seconds def valid_signature(raw_body: bytes, header: str, secret: str) -> bool: parts = [p.strip() for p in (header or "").split(",")] timestamp = next((p[2:] for p in parts if p.startswith("t=")), None) if timestamp is None or abs(time.time() - int(timestamp)) > TOLERANCE: return False signed = f"{timestamp}.".encode() + raw_body expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return any( hmac.compare_digest(p[3:], expected) for p in parts if p.startswith("v1=") ) ``` Verify against the **raw request body**, byte for byte. If your framework parses JSON before you can read the raw bytes, capture the raw body first — re-serializing changes whitespace and key order and the signature won't match. ## Secret rotation Rotate an endpoint's secret at any time with `POST /v4/webhook_endpoints/{id}/rotate_secret` — the response is the only place the new secret appears in plaintext. To avoid dropping deliveries while you roll it out, the old secret keeps verifying until the overlap window closes (the endpoint's `previous_secret_expires_at`). During the window Kit signs with **both** secrets, so the header carries two `v1` entries: ``` X-Kit-Signature: t=1753797130,v1=,v1= ``` This is why the examples above check whether **any** `v1` value matches — do that and your verification keeps working across a rotation with no code change. Once you've switched to the new secret you can close the window early with `POST /v4/webhook_endpoints/{id}/revoke_previous_secret`. Rotating again while a previous rotation's window is still open returns `409` — pass `force: true` to rotate anyway, immediately expiring the older secret. A [retried](/webhooks/retries) delivery is signed with whichever secrets are valid at send time, so a late retry that lands after the window closes carries a single `v1`. ## Idempotency The same event can arrive more than once — a [retry](/webhooks/retries) re-POSTs the whole delivery after your server was briefly unreachable, or an upstream job retry re-emits the event in a fresh delivery. Every event carries a UUID `id` that stays the same across re-sends: record it and skip any event you've already processed. Design your handler to be safe to run twice regardless.Deduplicate on the event `id`, not on `delivery_id` — a re-emitted event can arrive under a different delivery. # Welcome Source: https://developers.kit.com/welcome# Get a post Source: https://developers.kit.com/api-reference/posts/get-a-post /api-reference/v4.json get /v4/posts/{id} Returns a single post's full record, including its `content` HTML, `status` (e.g. `draft`, `scheduled`, `published`), `slug`, SEO fields (`description`, `meta_description`), thumbnail, `is_paid` flag, and the `public_url` where it's published on the creator's Kit site. A post that was also sent as a broadcast shares its `publication_id` with the matching broadcast — see [List broadcasts](/api-reference/broadcasts/list-broadcasts). # Create a purchase Source: https://developers.kit.com/api-reference/purchases/create-a-purchase /api-reference/v4.json post /v4/purchases Records a purchase against a subscriber, identified by `email_address` — use this to sync order data from your e-commerce platform into Kit so it appears on the subscriber's profile and can drive purchase-based automations. If no subscriber with that email address exists, one is created (in the `active` state) as part of the request. Provide the external `transaction_id` (required), monetary breakdown (`subtotal`, `tax`, `shipping`, `discount`, `total`), a 3-letter `currency` code (e.g. `USD`), `transaction_time`, and the purchased `products` — each product needs a `pid` (your platform's product identifier); products that don't exist yet are created automatically. **Note:** purchases are matched on `transaction_id`, so submitting one that already exists updates the existing purchase instead of creating a duplicate. The `products` in your request are *added* to that purchase as additional line items — they don't replace the ones already recorded — so resending the same products will duplicate the line items. Only include `products` you haven't already synced for that `transaction_id`. Returns `422` when required parameters are missing or invalid. # Get a purchase Source: https://developers.kit.com/api-reference/purchases/get-a-purchase /api-reference/v4.json get /v4/purchases/{id} Returns a single purchase's details: `transaction_id`, `status`, `source`, the buyer's `email_address` and `subscriber_id`, monetary breakdown (`subtotal`, `discount`, `tax`, `total`, `currency`), `transaction_time`, and the purchased `products` with quantities and unit prices. # List purchases Source: https://developers.kit.com/api-reference/purchases/list-purchases /api-reference/v4.json get /v4/purchases Returns a cursor-paginated list of purchases recorded in the account, typically imported from e-commerce platforms. Each purchase includes its `transaction_id`, `status`, `source` (e.g. the originating platform), the buyer's `email_address` and `subscriber_id`, monetary breakdown (`subtotal`, `discount`, `tax`, `total`, `currency`), and the purchased `products`. To record a new purchase, use [Create a purchase](/api-reference/purchases/create-a-purchase). # List segments Source: https://developers.kit.com/api-reference/segments/list-segments /api-reference/v4.json get /v4/segments Returns a cursor-paginated list of all segments in the account with their `id`, `name`, and creation date. Segments are created and managed in the Kit app — the API doesn't currently support creating or updating them. Use segment ids for targeting, for example in a broadcast's `subscriber_filter` (see [Create a broadcast](/api-reference/broadcasts/create-a-broadcast)). # Create a sequence email Source: https://developers.kit.com/api-reference/sequence-emails/create-a-sequence-email /api-reference/v4.json post /v4/sequences/{sequence_id}/emails Adds a single email to a sequence. Each sequence email represents one step subscribers receive on their journey through the sequence — its `position` determines order, and `delay_value` / `delay_unit` / `send_days` determine when it fires relative to either the previous email or the subscriber's entry. `subject`, `delay_value`, and `delay_unit` are required. **Day-based** emails (`delay_unit: "days"`) follow the parent sequence's `send_days` and `send_hour`; their per-email `send_days` (e.g. `["monday", "wednesday"]`) overrides the sequence schedule for that one email. **Hour-based** emails (`delay_unit: "hours"`) ignore the sequence schedule and fire once the delay elapses — Kit checks for hour-delayed sends every 15 minutes — and always return `send_days` as `null`. New emails are created in draft (`published: false`) so they don't go out until you flip the flag. `position` is auto-assigned to the end of the sequence if omitted. `content` accepts HTML and can embed snippets with `{{ snippet.# Integrate and automate with KitBuild apps for the Kit App Store or use Kit's API to automate custom workflows
Learn everything you need to know to build for the Kit App Store. Automate and access Kit programmatically via our API. Extend the creator Kit experience further, with native plugin functionality. Connect AI agents to Kit — query the developer docs in real time, or act on a creator's Kit account. Ask questions, get support, and connect with other developers. Stay up to date with the latest changes to Kit's developer platform. }}` — see [Create a snippet](/api-reference/snippets/create-a-snippet) — which Kit resolves at send time, so editing a snippet later updates every email referencing it without a re-publish. **Note:** only the first email in a sequence can be an immediate send (`delay_value: 0` with `delay_unit: "days"`). Subsequent emails need a positive delay. **Warning:** publishing an immediate first email, or inserting a new email at a `position` earlier than where existing subscribers sit, processes those subscribers and triggers a send right away. Plan around active sequences carefully. For end-user context on how creators build sequences in the Kit UI, see the help article on [creating and sending a sequence](https://help.kit.com/en/articles/2502629-creating-and-sending-a-sequence-in-kit). # Delete a sequence email Source: https://developers.kit.com/api-reference/sequence-emails/delete-a-sequence-email /api-reference/v4.json delete /v4/sequences/{sequence_id}/emails/{id} Permanently removes one email from a sequence. **Warning:** subscribers already queued to receive the deleted email will skip it — they'll continue to the next email in sequence order. This is intentional and not retryable. If subscribers need to receive the email's content, send a broadcast or re-add subscribers to the sequence (with `repeat: true`) before deleting. If you want to pause rather than delete, use [Update a sequence email](/api-reference/sequence-emails/update-a-sequence-email) with `published: false` instead. # Get a sequence email Source: https://developers.kit.com/api-reference/sequence-emails/get-a-sequence-email /api-reference/v4.json get /v4/sequences/{sequence_id}/emails/{id} Fetches a single sequence email by `id`. Unlike [List sequence emails](/api-reference/sequence-emails/list-sequence-emails), this endpoint **always returns the full `content`** — no `include_content` flag needed. Use this when you have an email's `id` and need the current body, timing, or publish state — for example, to render a preview before pushing an update via [Update a sequence email](/api-reference/sequence-emails/update-a-sequence-email). For the field semantics, see [Create a sequence email](/api-reference/sequence-emails/create-a-sequence-email). # List sequence emails Source: https://developers.kit.com/api-reference/sequence-emails/list-sequence-emails /api-reference/v4.json get /v4/sequences/{sequence_id}/emails Returns every email inside a sequence, ordered by `position` (the order subscribers receive them). Each entry carries timing (`delay_value`, `delay_unit`, `send_days`), publish state (`published`), and metadata (`subject`, `preview_text`, `email_template_id`). **Tip:** the heavier `content` field is omitted by default to keep responses fast on sequences with many emails. Pass `include_content=true` when you need the body — for example, to render a preview, audit Liquid usage, or sync an external draft. For the field semantics — particularly how `delay_unit` and `send_days` interact, and what happens when subscribers hit a `published: false` email — see [Create a sequence email](/api-reference/sequence-emails/create-a-sequence-email). # Update a sequence email Source: https://developers.kit.com/api-reference/sequence-emails/update-a-sequence-email /api-reference/v4.json put /v4/sequences/{sequence_id}/emails/{id} Updates any field on a sequence email — `subject`, `preview_text`, `content`, `delay_value`, `delay_unit`, `send_days`, `position`, `email_template_id`, or `published`. Only fields included in the request body change; everything else is preserved. **Note:** pass `send_days: null` to revert a per-email schedule override and inherit the parent sequence's `send_days` again. The response will then return all 7 days, indicating no per-email restriction. **Warning:** sending `send_days` on an email with `delay_unit: "hours"` returns `422` — `send_days` only applies to day-based emails. **Warning:** changing `position` while subscribers are actively progressing through the sequence can cause emails to be sent out of order or skipped. The same caution applies to flipping `published` on a `position: 0` email — it triggers Kit to process all queued subscribers for that email. For the sequence-email model and how `delay_unit` and `send_days` interact, see [Create a sequence email](/api-reference/sequence-emails/create-a-sequence-email). # Add subscriber to sequence Source: https://developers.kit.com/api-reference/sequences/add-subscriber-to-sequence /api-reference/v4.json post /v4/sequences/{sequence_id}/subscribers/{id} The subscriber being added to the sequence must already exist. Subscribers can be created using the "[Create a subscriber](#create-a-subscriber)" endpoint. # Add subscriber to sequence by email address Source: https://developers.kit.com/api-reference/sequences/add-subscriber-to-sequence-by-email-address /api-reference/v4.json post /v4/sequences/{sequence_id}/subscribers The subscriber being added to the sequence must already exist. Subscribers can be created using the "[Create a subscriber](#create-a-subscriber)" endpoint. # Create a sequence Source: https://developers.kit.com/api-reference/sequences/create-a-sequence /api-reference/v4.json post /v4/sequences Creates an empty sequence — the container that holds sequence emails. After creating the shell, use [Create a sequence email](/api-reference/sequence-emails/create-a-sequence-email) to populate it. Only `name` is required. Every other field has a sensible default: Kit fills in the account's default sending address, a daily send schedule, and the account time zone — and any of these can be tuned later via [Update a sequence](/api-reference/sequences/update-a-sequence). Two behavioural toggles worth flagging up front. `repeat` controls whether a subscriber can re-enter the sequence: by default a subscriber receives the emails once and is marked complete, but with `repeat: true`, re-adding the same subscriber via a Visual Automation, Rule, Bulk Action, or Import resets their position to the start. Filters and exclusions still apply across restarts. `hold` (evergreen) keeps subscribers active in the sequence after they've received every published email — useful when you plan to add more emails later. Without `hold`, subscribers transition to Completed and won't pick up future additions. `exclude_subscriber_sources` lets you exclude subscribers acquired via specific tags, sequences, forms, or segments — they'll skip this sequence entirely. For end-user context, see the help articles on [creating and sending a sequence](https://help.kit.com/en/articles/2502629-creating-and-sending-a-sequence-in-kit), [restarting a sequence](https://help.kit.com/en/articles/5022528-restart-a-sequence), and [holding subscribers in evergreen sequences](https://help.kit.com/en/articles/5192801-how-to-hold-subscribers-in-evergreen-sequences). # Delete a sequence Source: https://developers.kit.com/api-reference/sequences/delete-a-sequence /api-reference/v4.json delete /v4/sequences/{id} Soft-deletes a sequence. The sequence is removed from active delivery immediately, with cleanup of associated state happening in the background. **Warning:** deleting a sequence with active subscribers stops deliveries to those subscribers — they will not receive remaining emails, and any Visual Automations referencing the sequence will need to be updated. Confirm the sequence is not in active use before deleting. If you want to pause rather than delete, use [Update a sequence](/api-reference/sequences/update-a-sequence) with `active: false` instead. # Get a sequence Source: https://developers.kit.com/api-reference/sequences/get-a-sequence /api-reference/v4.json get /v4/sequences/{id} Fetches a single sequence by `id`. Use this when you need the current schedule, the `active` / `repeat` / `hold` flags, the configured `email_address` and `email_template_id`, or `exclude_subscriber_sources` for a known sequence — for example, to confirm settings before adding subscribers or to render an editor. For the individual emails inside the sequence, use [List sequence emails](/api-reference/sequence-emails/list-sequence-emails). For the sequence model and field semantics, see [Create a sequence](/api-reference/sequences/create-a-sequence). # List sequences Source: https://developers.kit.com/api-reference/sequences/list-sequences /api-reference/v4.json get /v4/sequences Returns every sequence on the account. A sequence is a self-contained set of automated emails — subscribers join, then receive each email in order, governed by per-email `delay_value` / `delay_unit` and the sequence's overall `send_days`, `send_hour`, and `time_zone` schedule. Each entry carries the schedule defaults plus three behavioural toggles: `active` (whether the sequence is delivering), `repeat` (whether subscribers can re-enter), and `hold` (whether subscribers stay active after receiving every published email — an evergreen pattern). See [Create a sequence](/api-reference/sequences/create-a-sequence) for the full sequence model. Once you have a sequence's `id`, [List sequence emails](/api-reference/sequence-emails/list-sequence-emails) returns the individual emails inside it. For end-user context on how creators build sequences, see the help articles on [creating and sending a sequence](https://help.kit.com/en/articles/2502629-creating-and-sending-a-sequence-in-kit) and [evergreen content](https://help.kit.com/en/articles/2502575-what-is-evergreen-content). # List subscribers for a sequence Source: https://developers.kit.com/api-reference/sequences/list-subscribers-for-a-sequence /api-reference/v4.json get /v4/sequences/{sequence_id}/subscribers Returns a cursor-paginated list of subscribers in the sequence, each with an `added_at` timestamp for when they entered it. By default only `active` subscribers are returned — use `status` (`active`, `inactive`, `bounced`, `complained`, `cancelled`, or `all`) to widen the search. Filter by when subscribers entered the sequence with `added_after`/`added_before`, or by when the subscriber record was created with `created_after`/`created_before`. To add a subscriber to the sequence, use [Add subscriber to sequence](/api-reference/sequences/add-subscriber-to-sequence). # Update a sequence Source: https://developers.kit.com/api-reference/sequences/update-a-sequence /api-reference/v4.json put /v4/sequences/{id} Updates any sequence settings — `name`, `email_address`, schedule (`send_days`, `send_hour`, `time_zone`), `email_template_id`, `exclude_subscriber_sources`, or the `active` / `repeat` / `hold` flags. Only fields included in the request body change; everything else is preserved. Some changes have user-visible side effects on subscribers already in the sequence: **Note:** flipping `active` from `false` to `true` resumes delivery for queued subscribers. Flipping it back to `false` pauses the sequence — subscribers stay in their current position but no new emails are sent until it's reactivated. **Note:** changing the schedule (`send_days`, `send_hour`, or `time_zone`) only affects future sends. It does not retroactively reschedule emails already queued for delivery. **Warning:** turning off `repeat` while subscribers are mid-sequence does not stop them from finishing — but they won't be re-eligible to start over after completing. See [Create a sequence](/api-reference/sequences/create-a-sequence) for the full sequence model and what each field controls. # Create a snippet Source: https://developers.kit.com/api-reference/snippets/create-a-snippet /api-reference/v4.json post /v4/snippets Snippets are reusable pieces of email content you can drop into a broadcast or sequence email using Liquid: `{{ snippet.key }}`. Update the snippet once and every email that references it picks up the new content on next send. There are two `snippet_type`s. **`inline`** snippets store plain-text content (with Liquid variable support like `{{ subscriber.first_name }}`) in the `content` field. **`block`** snippets store rich-text HTML — text, lists, images, buttons — in `document_attributes.value_html`. A snippet's type is fixed at creation: it cannot be changed via [Update a snippet](/api-reference/snippets/update-a-snippet). The response includes a `key` field. That's the identifier you use in Liquid — for example, a snippet returned with `"key": "welcome-message"` is referenced inside a broadcast as `{{ snippet.welcome-message }}`. Keys are derived from the snippet name on creation. **Note:** the API rejects circular references — a snippet cannot reference itself, directly or transitively — with a `422` validation error. For end-user context on how creators build and edit snippets in the Kit UI, see the help articles on [content snippets](https://help.kit.com/en/articles/3812712-creating-and-using-content-snippets-in-your-kit-emails) and [code snippets for custom templates](https://help.kit.com/en/articles/2810398-code-snippets-for-custom-email-templates). # Get a snippet Source: https://developers.kit.com/api-reference/snippets/get-a-snippet /api-reference/v4.json get /v4/snippets/{id} Fetches a single snippet by `id`. Unlike [List snippets](/api-reference/snippets/list-snippets), this endpoint **always returns the full `content` and `document`** — no `include_content` flag needed. Use this when you have an `id` (e.g. stored from a prior create call) and need the current `key` and body — for example, to preview the resolved HTML or confirm a snippet still exists before referencing it as `{{ snippet.key }}` in a broadcast or sequence email. See [Create a snippet](/api-reference/snippets/create-a-snippet) for the snippet model. # List snippets Source: https://developers.kit.com/api-reference/snippets/list-snippets /api-reference/v4.json get /v4/snippets Returns every snippet on the account. Each snippet's `key` is the identifier used in Liquid — `{{ snippet.key }}` — when creating a broadcast or sequence email. See [Create a snippet](/api-reference/snippets/create-a-snippet) for how snippets work end-to-end. **Tip:** the heavier `content` and `document` fields are omitted by default to keep responses fast. Pass `include_content=true` when you need the body — for example, to render a preview or audit Liquid usage. Filter the result with `snippet_type` (`inline` or `block`) and `archived` (defaults to `false`, set `true` to list only archived snippets). # Update a snippet Source: https://developers.kit.com/api-reference/snippets/update-a-snippet /api-reference/v4.json put /v4/snippets/{id} Rename a snippet, replace its body, or archive/restore it. Updates apply on the next send of any email that references the snippet via `{{ snippet.key }}` — there's no per-email versioning, so a content change ripples to every broadcast or sequence email using that key. The request body must match the existing `snippet_type`. For an **`inline`** snippet, send `content` (and optionally `name`, `archived`). For a **`block`** snippet, send `document_attributes.value_html` (and optionally `name`, `archived`). Pass `archived: true` to archive, `false` to restore. **Warning:** `snippet_type` is immutable. Sending a different value, or sending the body shape for the wrong type, returns a `422` with `snippet_type cannot be changed`. See [Create a snippet](/api-reference/snippets/create-a-snippet) for the full snippet model and how `key` ties into Liquid. # Bulk create subscribers Source: https://developers.kit.com/api-reference/subscribers/bulk-create-subscribers /api-reference/v4.json post /v4/bulk/subscribers See "[Bulk & async processing](#bulk-amp-async-processing)" for more information. # Create a subscriber Source: https://developers.kit.com/api-reference/subscribers/create-a-subscriber /api-reference/v4.json post /v4/subscribers Behaves as an upsert. If a subscriber with the provided email address does not exist, it creates one with the specified first name and state. If a subscriber with the provided email address already exists, it updates the first name.
If you include a custom field key that does not exist on your account, the request returns an error. Use [List custom fields](/api-reference/custom-fields/list-custom-fields) to retrieve existing keys, or [Create a custom field](/api-reference/custom-fields/create-a-custom-field) to add new fields before setting them for subscribers.
NOTE: Updating the subscriber state with this endpoint is not supported at this time.
NOTE: We support creating/updating a maximum of 140 custom fields at a time. # Delete a subscriber's location Source: https://developers.kit.com/api-reference/subscribers/delete-a-subscribers-location /api-reference/v4.json delete /v4/subscribers/{subscriber_id}/location Removes the subscriber's pinned location and returns `204 No Content` on success. After deletion, Kit may re-infer the subscriber's location from future open events. To set or replace a pinned location, use [Pin a subscriber's location](/api-reference/subscribers/pin-a-subscribers-location); to replace the values on an existing pin, use [Update a subscriber's pinned location](/api-reference/subscribers/update-a-subscribers-pinned-location). Returns `404` when the subscriber is not found. # Filter subscribers by engagement, sign-up date, state, and tags Source: https://developers.kit.com/api-reference/subscribers/filter-subscribers-by-engagement-sign-up-date-state-and-tags /api-reference/v4.json post /v4/subscribers/filter Searches your subscriber list with compound conditions: email engagement (opens, clicks, sends, deliveries — with count thresholds and date ranges), sign-up date, subscriber state, and tags. Every condition in the `all` array must match (AND logic). Use `counting_mode` to control how engagement thresholds are tallied — `raw` (default) counts every event, `unique_email` counts distinct emails — and the `include` array to embed extra fields (tags, stats, custom fields, location, attribution, canceled_at) on each returned subscriber. **Data retention:** Starting October 15, 2026, email stats (opens, clicks, sends, bounces, and unsubscribes) are available via the API for the last 5 years — engagement-condition date bounds are clamped to that window, and a `stats` include range outside it returns a `400` error. See [Email data retention](/api-reference/email-data-retention) for the full policy. # Get a subscriber Source: https://developers.kit.com/api-reference/subscribers/get-a-subscriber /api-reference/v4.json get /v4/subscribers/{id} Returns a single subscriber's record: first name, email address, `state` (e.g. `active` or `cancelled`), creation date, and their custom field values under `fields`. To look a subscriber up by email address instead of id, use [List subscribers](/api-reference/subscribers/list-subscribers) with the `email_address` parameter. For their email engagement history, see [List stats for a subscriber](/api-reference/subscribers/list-stats-for-a-subscriber); for their tags, see [List tags for a subscriber](/api-reference/subscribers/list-tags-for-a-subscriber). # List stats for a subscriber Source: https://developers.kit.com/api-reference/subscribers/list-stats-for-a-subscriber /api-reference/v4.json get /v4/subscribers/{subscriber_id}/stats Retrieve email stats for a specific subscriber: totals for sent, opened, clicked, and bounced emails, open and click rates, and recency signals (`last_sent`, `last_opened`, `last_clicked`, and sends since the last open/click). Filter the stats with the `email_sent_after` and/or `email_sent_before` query parameters to limit them to emails sent within a specific date range. **Data retention:** Starting October 15, 2026, email stats (opens, clicks, sends, bounces, and unsubscribes) are available via the API for the last 5 years — `email_sent_after`/`email_sent_before` are clamped to that window, and dates outside it return a `400` error indicating the earliest available date. Older data is archived and can no longer be retrieved via the API; creators can [contact Kit support](https://help.kit.com) to request an export of their archived data. See [Email data retention](/api-reference/email-data-retention) for the full policy. # List subscribers Source: https://developers.kit.com/api-reference/subscribers/list-subscribers /api-reference/v4.json get /v4/subscribers Returns a cursor-paginated list of subscribers in the account. By default only `active` subscribers are returned — use `status` (`active`, `inactive`, `bounced`, `complained`, `cancelled`, or `all`) to widen the search. Look a subscriber up by exact `email_address`, filter by `created_after`/`created_before` and `updated_after`/`updated_before`, and order with `sort_field` (`id`, `created_at`, `updated_at`, `cancelled_at`, or an `engagement__`) and `sort_order`. Use `include` (comma-separated: `attribution`, `tags`, `location`, `canceled_at`) to embed extra fields on each subscriber — including `canceled_at` without `status=cancelled` returns a `422`. Set `slim=true` to omit the `fields` object (custom field values) for a faster, smaller response. Ordering by an engagement metric (opens, clicks, sends over the trailing 90 days) is supported here via `sort_field=engagement__ `, but cannot be combined with an `email_address` filter. To *filter* subscribers by engagement, use [Filter subscribers by engagement](/api-reference/subscribers/filter-subscribers-by-engagement-sign-up-date-state-and-tags) instead. Results are eventually consistent and may briefly lag very recent changes — see [Eventual consistency](/api-reference/eventual-consistency). # List tags for a subscriber Source: https://developers.kit.com/api-reference/subscribers/list-tags-for-a-subscriber /api-reference/v4.json get /v4/subscribers/{subscriber_id}/tags Returns a cursor-paginated list of the tags currently applied to a subscriber, including when each was applied (`tagged_at`). To see every tag in the account, use [List tags](/api-reference/tags/list-tags). To add or remove a tag for this subscriber, use [Tag a subscriber](/api-reference/tags/tag-a-subscriber) and [Remove tag from subscriber](/api-reference/tags/remove-tag-from-subscriber). # Pin a subscriber's location Source: https://developers.kit.com/api-reference/subscribers/pin-a-subscribers-location /api-reference/v4.json post /v4/subscribers/{subscriber_id}/location Pins an explicit location to a subscriber, overriding any location Kit has inferred from open events. Provide `city`, `state_province`, `country_code` (ISO 3166-1 alpha-2, required), `latitude`, `longitude`, and `timezone` (IANA timezone name, e.g. `America/Denver`). Returns the subscriber's `id` and the pinned `location` on success. If the subscriber already has a pinned location this replaces it. To replace the values on an existing pin, use [Update a subscriber's pinned location](/api-reference/subscribers/update-a-subscribers-pinned-location) (PATCH); to remove the pin, use [Delete a subscriber's location](/api-reference/subscribers/delete-a-subscribers-location). The pinned location is returned on [Get a subscriber](/api-reference/subscribers/get-a-subscriber). Returns `404` when the subscriber is not found, `422` when a required field is missing or invalid. # Unsubscribe subscriber Source: https://developers.kit.com/api-reference/subscribers/unsubscribe-subscriber /api-reference/v4.json post /v4/subscribers/{id}/unsubscribe Unsubscribes the subscriber from all future emails, moving them to the `cancelled` state. Returns `204 No Content` on success. The subscriber record, history, and tags are retained — they are not deleted from the account. **Note:** this is the API equivalent of the subscriber clicking unsubscribe. Treat it as consent-revoking and effectively permanent — only re-subscribe someone with their explicit permission. # Update a subscriber Source: https://developers.kit.com/api-reference/subscribers/update-a-subscriber /api-reference/v4.json put /v4/subscribers/{id} If you include a custom field key that does not exist on your account, the request returns an error. Use [List custom fields](/api-reference/custom-fields/list-custom-fields) to retrieve existing keys, or [Create a custom field](/api-reference/custom-fields/create-a-custom-field) to add new fields before setting them for subscribers.
NOTE: We support creating/updating a maximum of 140 custom fields at a time. # Update a subscriber's pinned location Source: https://developers.kit.com/api-reference/subscribers/update-a-subscribers-pinned-location /api-reference/v4.json patch /v4/subscribers/{subscriber_id}/location Updates a subscriber's pinned location. This is a full replacement, not a partial update: `city`, `state_province`, `country_code` (ISO 3166-1 alpha-2), `latitude`, `longitude`, and `timezone` (IANA timezone name, e.g. `America/Denver`) are all required, so resend the current value for any field you are not changing. Omitting one returns `422`. Returns the subscriber's `id` and the updated `location` on success. To create a pin from scratch, use [Pin a subscriber's location](/api-reference/subscribers/pin-a-subscribers-location) (POST); to remove the pin entirely, use [Delete a subscriber's location](/api-reference/subscribers/delete-a-subscribers-location). Returns `404` when the subscriber is not found, `422` when a required field is missing or invalid. # Bulk create tags Source: https://developers.kit.com/api-reference/tags/bulk-create-tags /api-reference/v4.json post /v4/bulk/tags See "[Bulk & async processing](#bulk-amp-async-processing)" for more information. # Bulk delete tags Source: https://developers.kit.com/api-reference/tags/bulk-delete-tags /api-reference/v4.json delete /v4/bulk/tags See "[Bulk & async processing](/api-reference/bulk-and-async-processing)" for more information. # Bulk remove tags from subscribers Source: https://developers.kit.com/api-reference/tags/bulk-remove-tags-from-subscribers /api-reference/v4.json delete /v4/bulk/tags/subscribers See "[Bulk & async processing](#bulk-amp-async-processing)" for more information. # Bulk tag subscribers Source: https://developers.kit.com/api-reference/tags/bulk-tag-subscribers /api-reference/v4.json post /v4/bulk/tags/subscribers The subscribers being tagged must already exist. Subscribers can be created in bulk using the "[Bulk create subscriber](#bulk-create-subscribers)" endpoint.
See "[Bulk & async processing](#bulk-amp-async-processing)" for more information. # Create a tag Source: https://developers.kit.com/api-reference/tags/create-a-tag /api-reference/v4.json post /v4/tags Creates a tag with the given `name`. Creation is idempotent on name (matched case-insensitively): if a tag with that name already exists, the endpoint returns `200` with the existing tag instead of creating a duplicate (a newly created tag returns `201`). Returns `422` if `name` is missing. To create many tags in one request, use [Bulk create tags](/api-reference/tags/bulk-create-tags). # List subscribers for a tag Source: https://developers.kit.com/api-reference/tags/list-subscribers-for-a-tag /api-reference/v4.json get /v4/tags/{tag_id}/subscribers Returns a cursor-paginated list of subscribers who have this tag. By default only `active` subscribers are returned — use `status` (`active`, `inactive`, `bounced`, `complained`, `cancelled`, or `all`) to widen the search. Filter by when the tag was applied with `tagged_after`/`tagged_before` (format `yyyy-mm-dd`), or by when the subscriber joined with `created_after`/`created_before`. Set `slim=true` to omit expensive optional fields for a faster, smaller response. # List tags Source: https://developers.kit.com/api-reference/tags/list-tags /api-reference/v4.json get /v4/tags Returns a cursor-paginated list of all tags in the account with their `id`, `name`, and creation date. Add `include=subscriber_count` to embed each tag's subscriber count. Tags are the main building block for segmenting your list: apply one with [Tag a subscriber](/api-reference/tags/tag-a-subscriber), and see who has one with [List subscribers for a tag](/api-reference/tags/list-subscribers-for-a-tag). # Remove tag from subscriber Source: https://developers.kit.com/api-reference/tags/remove-tag-from-subscriber /api-reference/v4.json delete /v4/tags/{tag_id}/subscribers/{id} Removes the tag from the subscriber and returns `204 No Content`. The subscriber stays in your account and keeps their other tags — only this tag association is removed. Returns `404` when the tag or subscriber id doesn't exist. To identify the subscriber by email address instead of id, use [Remove tag from subscriber by email address](/api-reference/tags/remove-tag-from-subscriber-by-email-address). To remove tags from many subscribers at once, use [Bulk remove tags from subscribers](/api-reference/tags/bulk-remove-tags-from-subscribers). # Remove tag from subscriber by email address Source: https://developers.kit.com/api-reference/tags/remove-tag-from-subscriber-by-email-address /api-reference/v4.json delete /v4/tags/{tag_id}/subscribers Removes the tag from a subscriber identified by the `email_address` query parameter, returning `204 No Content`. The subscriber stays in your account and keeps their other tags — only this tag association is removed. This is the email-address variant of [Remove tag from subscriber](/api-reference/tags/remove-tag-from-subscriber) — use it when you have the subscriber's email but not their `id`. Returns `404` when the tag doesn't exist or no subscriber matches the email address. # Tag a subscriber Source: https://developers.kit.com/api-reference/tags/tag-a-subscriber /api-reference/v4.json post /v4/tags/{tag_id}/subscribers/{id} The subscriber being tagged must already exist. Subscribers can be created using the "[Create a subscriber](#create-a-subscriber)" endpoint. # Tag a subscriber by email address Source: https://developers.kit.com/api-reference/tags/tag-a-subscriber-by-email-address /api-reference/v4.json post /v4/tags/{tag_id}/subscribers The subscriber being tagged must already exist. Subscribers can be created using the "[Create a subscriber](#create-a-subscriber)" endpoint. # Update tag name Source: https://developers.kit.com/api-reference/tags/update-tag-name /api-reference/v4.json put /v4/tags/{id} Renames a tag. The tag keeps its `id` and all of its subscribers — only the `name` changes, and the new name is reflected everywhere the tag is used (forms, automations, filters). Returns `422` when the new name is blank or already used by another tag in the account (names are unique, case-insensitive). # Create a webhook Source: https://developers.kit.com/api-reference/webhooks-legacy/create-a-webhook /api-reference/v4.json post /v4/webhooks **Legacy:** This creates a previous-generation webhook. Existing webhooks keep working and there are no plans to remove them, but new integrations should use [Create a webhook endpoint](/api-reference/webhooks/create-a-webhook-endpoint) instead — one endpoint subscribes to many event types and gets signed, automatically retried deliveries. See the [webhooks guides](/webhooks/overview). Available event types:
- `subscriber.subscriber_activate`
- `subscriber.subscriber_unsubscribe`
- `subscriber.subscriber_bounce`
- `subscriber.subscriber_complain`
- `subscriber.form_subscribe`, required parameter `form_id` [Integer]
- `subscriber.course_subscribe`, required parameter `sequence_id` [Integer]
- `subscriber.course_complete`, required parameter `sequence_id` [Integer]
- `subscriber.link_click`, required parameter `initiator_value` [String] as a link URL
- `subscriber.product_purchase`, required parameter `product_id` [Integer]
- `subscriber.tag_add`, required parameter `tag_id` [Integer]
- `subscriber.tag_remove`, required parameter `tag_id` [Integer]
- `purchase.purchase_create`
- `custom_field.field_created`
- `custom_field.field_deleted`
- `custom_field.field_value_updated`, required parameter `custom_field_id` [Integer] # Delete a webhook Source: https://developers.kit.com/api-reference/webhooks-legacy/delete-a-webhook /api-reference/v4.json delete /v4/webhooks/{id} **Legacy:** This deletes a previous-generation webhook. If you're replacing it rather than retiring it, consider recreating it as a [webhook endpoint](/api-reference/webhooks/create-a-webhook-endpoint) — see the [webhooks guides](/webhooks/overview). Deletes the webhook and stops all future deliveries of its subscribed events to the target URL. Returns `204 No Content` on success, `404` when the id doesn't exist, and `422` when the webhook cannot be deleted. Find webhook ids with [List webhooks](/api-reference/webhooks-legacy/list-webhooks); register a replacement with [Create a webhook](/api-reference/webhooks-legacy/create-a-webhook). # List webhooks Source: https://developers.kit.com/api-reference/webhooks-legacy/list-webhooks /api-reference/v4.json get /v4/webhooks **Legacy:** This is the previous generation of Kit webhooks. It keeps working and there are no plans to remove it, but it's superseded by [webhook endpoints](/api-reference/webhooks/list-webhook-endpoints), which support many event types per endpoint, signed deliveries, and automatic retries — use those for new integrations. See the [webhooks guides](/webhooks/overview). Webhooks are automations that will receive subscriber data when a subscriber event is triggered, such as when a subscriber completes a sequence.
When a webhook is triggered, a `POST` request will be made to your URL with a JSON payload. # Create a webhook endpoint Source: https://developers.kit.com/api-reference/webhooks/create-a-webhook-endpoint /api-reference/v4.json post /v4/webhook_endpoints Registers a URL to receive deliveries for the [event types](/webhooks/event-types) listed in `events`. The `url` must be publicly reachable over HTTP(S) — private, internal, and loopback addresses are rejected. **The response is the only time the signing `secret` is returned in plaintext.** Store it securely; you need it to [verify the signature](/webhooks/verifying-signatures) on every delivery. If you lose it, [rotate the secret](/api-reference/webhooks/rotate-a-webhook-endpoint-secret) to get a new one. **Note:** This resource supersedes the [legacy Webhooks](/api-reference/webhooks-legacy/create-a-webhook) resource. Legacy webhooks keep working, but new integrations should be built here — see [getting started](/webhooks/getting-started) for a full walkthrough. # Delete a webhook endpoint Source: https://developers.kit.com/api-reference/webhooks/delete-a-webhook-endpoint /api-reference/v4.json delete /v4/webhook_endpoints/{id} Deletes the webhook endpoint and stops all future deliveries of its subscribed events. Returns `204 No Content` on success and `404` when the id doesn't exist or isn't visible to the caller. Endpoints created by an app (via OAuth) can only be deleted by that app; an API key request returns `403`. To stop deliveries temporarily instead, [update](/api-reference/webhooks/update-a-webhook-endpoint) the endpoint's `status` to `disabled`. # Get a webhook endpoint Source: https://developers.kit.com/api-reference/webhooks/get-a-webhook-endpoint /api-reference/v4.json get /v4/webhook_endpoints/{id} Returns a single webhook endpoint. The signing `secret` is never included — it's only returned when you [create the endpoint](/api-reference/webhooks/create-a-webhook-endpoint) or [rotate the secret](/api-reference/webhooks/rotate-a-webhook-endpoint-secret). Returns `404` when the id doesn't exist, belongs to another account, or belongs to an endpoint created by a different app. # List webhook endpoints Source: https://developers.kit.com/api-reference/webhooks/list-webhook-endpoints /api-reference/v4.json get /v4/webhook_endpoints Returns a paginated list of the account's webhook endpoints. Filter with `status` to return only `active` or `disabled` endpoints. Webhook endpoints are the current generation of Kit webhooks: one endpoint subscribes to many [event types](/webhooks/event-types) and receives signed, automatically retried deliveries. They supersede the [legacy Webhooks](/api-reference/webhooks-legacy/list-webhooks) resource — which keeps working — and are the right choice for all new integrations. Start with the [webhooks guides](/webhooks/overview). # Revoke the previous webhook endpoint secret Source: https://developers.kit.com/api-reference/webhooks/revoke-the-previous-webhook-endpoint-secret /api-reference/v4.json post /v4/webhook_endpoints/{id}/revoke_previous_secret Closes a [rotation's](/api-reference/webhooks/rotate-a-webhook-endpoint-secret) overlap window early: the previous secret stops verifying immediately, leaving only the current secret valid. Call this once you've fully switched to the new secret, rather than waiting for `previous_secret_expires_at` to pass. Subsequent deliveries are signed with the current secret only, so the `X-Kit-Signature` header goes back to carrying a single `v1` entry. # Rotate a webhook endpoint secret Source: https://developers.kit.com/api-reference/webhooks/rotate-a-webhook-endpoint-secret /api-reference/v4.json post /v4/webhook_endpoints/{id}/rotate_secret Generates a new signing secret for the endpoint and returns it. **The response is the only time the new `secret` appears in plaintext** — store it securely. To avoid dropping deliveries mid-rollout, the previous secret keeps verifying until the overlap window closes (the endpoint's `previous_secret_expires_at`). During the window every delivery is signed with both secrets, so the `X-Kit-Signature` header carries two `v1` entries — see [secret rotation](/webhooks/verifying-signatures#secret-rotation). Rotating again while a previous rotation's window is still open returns `409`; pass `force: true` to rotate anyway and immediately expire the older secret. Once you've switched over, you can close the window early with [Revoke the previous secret](/api-reference/webhooks/revoke-the-previous-webhook-endpoint-secret). # Update a webhook endpoint Source: https://developers.kit.com/api-reference/webhooks/update-a-webhook-endpoint /api-reference/v4.json patch /v4/webhook_endpoints/{id} Updates any of `name`, `url`, `description`, `status`, and `events`. Set `status` to `disabled` to stop deliveries without deleting the endpoint, and back to `active` to resume. **Note:** `events` replaces the endpoint's entire subscription list — send the complete set you want, not just the additions. Endpoints created by an app (via OAuth) can only be updated by that app; an API key request returns `403`. # Changelog Source: https://developers.kit.com/changelog Developer platform updates and new features.## 🚀 Webhooks 2.0: endpoint-based webhooks with signed, batched deliveries Webhooks are now a first-class platform feature. Register an endpoint once - via the new `/v4/webhook_endpoints` API or the new [Webhooks page](https://app.kit.com/webhooks) in the Kit app - and subscribe it to any combination of events. Apps can also register endpoints on the accounts that install them. * **Signed deliveries** - every delivery includes a signature so you can [verify it came from Kit](https://developers.kit.com/webhooks/verifying-signatures), and signing secrets can be retrieved and rotated after creation. * **Batched** - bulk activity (like tagging hundreds of subscribers) arrives as deliveries of up to 100 events, not individual requests. * **Automatic retries** - when your endpoint is unavailable, Kit [retries each delivery](https://developers.kit.com/webhooks/retries) 8 times over roughly 41 hours. * **Growing event catalogue** - event types are rolling out incrementally; the [event reference](https://developers.kit.com/webhooks/event-types) shows what delivers today and what's planned. Existing v4 webhooks keep working unchanged (now documented as legacy webhooks) - no migration required. Get started: [Webhooks overview](https://developers.kit.com/webhooks/overview) · [Getting started guide](https://developers.kit.com/webhooks/getting-started) ## 🚀 Sort subscribers by engagement metrics `GET /v4/subscribers` and `GET /v4/subscribers` now accept engagement sort values for `sort_field`: `engagement__sent`, `engagement__opens`, `engagement__clicks`, `engagement__open_rate`, and `engagement__click_rate`. Each ranks subscribers by their trailing-90-day engagement count or rate; subscribers with no sends sort as 0. ## 🚀 Sequence stats are now available via API You can now retrieve sequence and sequence email performance directly from the API. Add `include=stats` to any of the following endpoints and each sequence (or email) will include a `stats` object: * `GET /v4/sequences` * `GET /v4/sequences/{id}` * `GET /v4/sequences/{id}/emails` * `GET /v4/sequences/{id}/emails/{id}` Sequence-level stats include recipients, opens, clicks, unsubscribes, email unsubscribes, bounces, complaints, and the corresponding rates (open rate, click rate, click-to-open rate, unsubscribe rate, bounce rate, complaint rate). Per-email stats include the same set, minus unsubscribes. The parameter is fully opt-in — without it, responses are unchanged. These are the same numbers that power your in-app sequences dashboard, so the figures match what you see in Kit. See the docs to get started: * [List sequences](https://developers.kit.com/api-reference/sequences/list-sequences) * [List sequence emails](https://developers.kit.com/api-reference/sequence-emails/list-sequence-emails) ## 🚀 New `plan` object on `GET /v4/account` The current account endpoint now returns a nested **`plan`** object with plan and trial details, so integrations can tell paying accounts apart from trialing ones without extra lookups: * **`plan_type`** — the account's plan type; the same value as the existing top-level `plan_type`, duplicated for convenience. * **`interval`** — the plan's billing interval (`month` or `year`). * **`subscriber_limit`** — the plan's subscriber tier; `null` means unlimited. * **`on_trial`** — `true` while the account is on an active free trial. * **`trial_lapse_date`** — when the account's most recent trial ends or ended; `null` if the account never had a trial. * **`renews_at`** — when the subscription will renew; only set while the subscription is active and will actually renew. * **`cancels_at`** — when a cancelled-but-still-paid-up subscription's access ends; only set while a cancellation is pending. At most one of `renews_at` / `cancels_at` is non-null. All existing fields are unchanged, so no integration updates are required. Full reference: [Get current account](https://developers.kit.com/api-reference/accounts/get-current-account) ## 🚀 Preview your app listing before saving The app details editor now has a **Preview** button (next to **Save**) that renders your App Store listing with your current unsaved changes — name, summary, icon, images, video, categories, and the markdown description — exactly as creators will see it. It uses the same server-side markdown rendering as the live page, so it's also the quickest way to check which formatting is supported. You can save directly from the preview; published apps still get the go-live confirmation. Rolling out to app developers now. More: [App details page](https://developers.kit.com/kit-app-store/app-details-page) ## 🔧 Sequence email `position` is now guaranteed unique Positions within a sequence are now enforced as unique at every write path, so two active emails can no longer share a slot: * **Explicit collisions are rejected** — `POST` and `PUT` on `/v4/sequences/{sequence_id}/emails` return **422** if the requested `position` is already taken by another email in that sequence. * **Omit `position` to append** — leaving `position` out (or sending `null`) now allocates the next available slot at the end of the sequence instead of writing an empty position. Full reference: [Create a sequence email](https://developers.kit.com/api-reference/sequence-emails/create-a-sequence-email) · [Update a sequence email](https://developers.kit.com/api-reference/sequence-emails/update-a-sequence-email) ## 🚀 `POST /v4/subscribers/filter` gains custom fields, location, attribution, and includes The subscriber filter endpoint picked up a series of upgrades across June, making it a much more capable segmentation tool: * **`custom_field` filter** — match subscribers on any custom field value. * **`location` filter** — match subscribers by location. * **Attribution filter** — match subscribers by the form or landing page they were attributed to. * **`counting_mode` on engagement filters** — choose `raw` (every engagement event) or `unique_email` (one per email) when filtering on opens/clicks. * **`include` body parameter** — request extra data alongside each match, including `stats` (engagement stats) and `custom_fields` (custom field values). These compose with the existing engagement, `subscribed`, `subscriber_state`, and `tags` filters in the same request. Default behaviour is unchanged — the new conditions and includes are all opt-in. Full reference: [Filter subscribers](https://developers.kit.com/api-reference/subscribers/filter-subscribers-by-engagement-sign-up-date-state-and-tags) ## 🚀 Purchases now expose `source` and include subscription & tip line items Two additions to the v4 Purchases API: * Every purchase response now includes a **`source`** field — the name of the originating integration or connected app (e.g. `Gumroad`, `Stripe`, `Teachable`) — on `GET /v4/purchases`, `GET /v4/purchases/{id}`, and the create response. * Purchase responses now **include subscription and tip line items** alongside standard product items, so recurring-revenue and tip transactions are fully represented. Full reference: [List purchases](https://developers.kit.com/api-reference/purchases/list-purchases) · [Create a purchase](https://developers.kit.com/api-reference/purchases/create-a-purchase) ## 🚀 New endpoint: `DELETE /v4/bulk/tags` You can now delete multiple tags in a single request with the new bulk endpoint, mirroring the existing bulk create/tag patterns. Full reference: [Bulk delete tags](https://developers.kit.com/api-reference/tags/bulk-delete-tags) ## 🚀 Opt-in `subscriber_count` on `GET /v4/tags` and `GET /v4/forms` Both index endpoints can now return an active **`subscriber_count`** per record when you request it with **`?include=subscriber_count`**. It's opt-in so default responses stay fast — pass the include only when you need the counts. Full reference: [List tags](https://developers.kit.com/api-reference/tags/list-tags) · [List forms](https://developers.kit.com/api-reference/forms/list-forms) ## 🚀 Engagement rates on `GET /v4/account/email_stats` The account email stats endpoint now returns four calculated rates alongside the raw counts: **`open_rate`**, **`click_rate`**, **`unsubscribe_rate`**, and **`bounce_rate`**. You no longer need to derive these client-side from the underlying totals. Full reference: [Get email stats](https://developers.kit.com/api-reference/accounts/get-email-stats) ## 🔧 `POST /oauth/revoke` now performs full partner-side cleanup When a partner revokes a Kit-issued OAuth token, Kit now tears down the linked install state in the same request: * Any partner credentials Kit had stored for that account are queued for deletion. * The Kit-side installation record is marked uninstalled, so the app no longer appears as installed for that account. Previously, revoking a token only invalidated the access grant — installation state and stored credentials lingered until the next sync. App developers who treat `/oauth/revoke` as a clean teardown signal can now rely on it as a single call. The HTTP contract of `/oauth/revoke` is unchanged; both the request and the success response remain identical. Full reference: [OAuth token revocation](https://developers.kit.com/api-reference/oauth-token-revocation) ## 🚀 Broadcast `status` field and `?status=` filter on broadcast list endpoints Every V4 broadcast response now includes a `status` field with one of `draft`, `scheduled`, `sending`, `completed`, or `aborted`. This removes the need to infer broadcast state from a mix of `send_at`, `published_at`, and other fields. `GET /v4/broadcasts` and `GET /v4/broadcasts/stats` also accept a new optional `?status=` query parameter that filters to broadcasts in the given lifecycle state. Passing an unknown value returns `422 Unprocessable Entity` with the list of accepted statuses. Omitting `status` preserves existing behaviour. Full reference: [List broadcasts](https://developers.kit.com/api-reference/broadcasts/list-broadcasts) · [Get stats for a list of broadcasts](https://developers.kit.com/api-reference/broadcasts/get-stats-for-a-list-of-broadcasts) ## 🚀 `slim` query parameter on `/v4/subscribers` and `/v4/forms/:form_id/subscribers` Two more list endpoints now accept `?slim=true`, extending the slim mode introduced for `GET /v4/broadcasts` and `GET /v4/tags/:tag_id/subscribers` on May 20. In slim mode, the `fields` object (custom field values) is omitted from each subscriber record and the underlying query skips the custom field join entirely, making large list responses noticeably faster. Default behaviour is unchanged — pass `slim=true` explicitly to opt in. On `/v4/subscribers`, slim composes with the existing `include=` parameter — the requested includes still resolve, slim only skips the `fields` block. `/v4/forms/:form_id/subscribers` doesn't accept `include=`. Full reference: [List subscribers](https://developers.kit.com/api-reference/subscribers/list-subscribers) · [List subscribers for a form](https://developers.kit.com/api-reference/forms/list-subscribers-for-a-form) ## 🚀 New `subscriber_state` filter on `/v4/subscribers/filter` `POST /v4/subscribers/filter` accepts a new filter `type`: `subscriber_state`. Provide a `states` array of one or more lifecycle values — `active`, `inactive`, `bounced`, `cancelled`, or `complained` — and the filter returns subscribers whose current state matches any value in the array. `subscriber_state` composes with engagement, `subscribed`, and `tags` filters in the same request. `since` / `before` and `any` don't apply to this type and are ignored when present. Most workflows want `states: ["active"]` — that mirrors the default scope of most other subscriber-facing queries in Kit. Full reference: [Filter subscribers based on engagement](https://developers.kit.com/api-reference/subscribers/filter-subscribers-based-on-engagement) ## 🔧 OAuth refresh token rotation Refresh tokens are now single-use. Each call to `POST /oauth/token` with `grant_type=refresh_token` issues a new refresh token and revokes the previous one. Reusing an old refresh token returns `invalid_grant`. This affects every OAuth client connected to Kit — including App Store integrations and MCP clients. If you store the refresh token from the original authorisation flow and reuse it across multiple sessions, update your client to store and use the latest refresh token from each refresh response instead. Mainstream OAuth clients handle rotation natively; custom integrations that cache refresh tokens will need a small update. ## 🔧 OAuth consent screen — DCR response now RFC 7591-aligned Public OAuth clients (Dynamic Client Registration with `confidential: false`) no longer receive `client_secret` or `client_secret_expires_at` in the DCR registration response. The `token_endpoint_auth_method` field is now `none` for public clients. The `.well-known/oauth-authorization-server` discovery document now advertises `token_endpoint_auth_methods_supported: ["client_secret_post", "none"]`. Existing well-formed DCR clients work without changes — this aligns the response with what the server already accepted. Full reference: [OAuth refresh token flow](https://developers.kit.com/api-reference/oauth-refresh-token-flow) ## 🚀 New fields on V4 sequence responses `GET /v4/sequences` and `GET /v4/sequences/{id}` now return two new fields: `email_count` and `subscriber_count`. `email_count` reflects published email templates only (drafts don't count); `subscriber_count` reflects active subscribers. Both are available to all V4 callers (API key and OAuth) — no changes required on your end. ## 🚀 New `slim` query parameter on heavy list endpoints `GET /v4/broadcasts` and `GET /v4/tags/:tag_id/subscribers` now accept `?slim=true` to skip expensive fields and database queries. For broadcasts, slim mode omits `content`, `public_url`, `email_address`, `email_template`, and `subscriber_filter`. For tag subscribers, it omits custom field values. Default behaviour is unchanged — pass `slim=true` explicitly to opt in. Full reference: [List broadcasts](https://developers.kit.com/api-reference/broadcasts/list-broadcasts) · [List subscribers for a tag](https://developers.kit.com/api-reference/tags/list-subscribers-for-a-tag) · [Get a sequence](https://developers.kit.com/api-reference/sequences/get-a-sequence) ## 🚀 Visual Automation templates for all app developers App developers can now build, submit, and publish **Visual Automation templates** directly from the developer portal — pre-built automations that creators preview and adopt the moment they install your app. * Templates move through *Draft → In review → Approved → Published* states, managed from your app's **Templates** tab. * Template review runs alongside [app review](/kit-app-store/going-live) — bundled with app review for unpublished apps, template-only for published apps. * Editing a published template's name, description, feature labels, or source automation auto-unpublishes it and sends it back for re-review. The last-approved version stays live for creators until re-approval. * Deleting a template removes it from your catalogue; creators who've already adopted it keep their working copy. * Once a creator adopts a published template, the automation editor is locked — they can see it but can't change it. [Visual Automation templates](/kit-app-store/va-templates) ## 🚀 Dynamic content blocks and Visual Automation context are now available to all developers Two related capabilities are out of limited release and available to every Kit app developer. **Dynamic content blocks** — content block plugins that return a Liquid template instead of static HTML, so the rendered output is personalized per subscriber at send time. Configure your event plugin's **Sample data** JSON on the Context card, link a content block to it via `related_plugin_id`, and reference subscriber data in Liquid as `{{ automation. . .* }}`. Full guide: [Dynamic content blocks](/plugins/content-blocks/dynamic-blocks/overview). **Visual Automation context** — event plugins can now attach an optional `context` object to each event, and Kit makes that context available everywhere downstream: * In **sequence email** Liquid (body, button URLs, links): `{{ automation. . . }}`. * On **dynamic content block** previews and per-subscriber renders. * On **action plugin** request bodies as `subscribers[].context.apps. . .*` — your action endpoint receives the same context your event plugin (or another app's event plugin) attached upstream, with no extra lookup. **Limits:** 64 KB per subscriber context row, 32 distinct app namespaces, 64 keys per app. Over-limit data is dropped silently rather than blocking the VA. See [automation node plugin configuration → Event context](/plugins/automation-nodes/plugin-configuration#event-context) and [Receiving VA context on action payloads](/plugins/automation-nodes/plugin-configuration#receiving-va-context-on-action-payloads). ## 🚀 `GET /v4/broadcasts/stats` — enriched response and date filtering `GET /v4/broadcasts/stats` now returns `subject` and `send_at` on every row by default. This eliminates the need for a follow-up `GET /v4/broadcasts/:id` call per row when building subject-line or send-time analyses. The endpoint also accepts two new optional query parameters: * `sent_after` — filter to broadcasts sent after this ISO 8601 datetime * `sent_before` — filter to broadcasts sent before this ISO 8601 datetime Omitting both preserves existing response behaviour — no breaking change. Cursor pagination via `per_page` / `after` continues to work as documented. **Plan gating:** this endpoint requires a Pro plan or special developer authorisation. Requests from free accounts return `403`. Full reference: [/api-reference/broadcasts/get-stats-for-a-list-of-broadcasts](https://developers.kit.com/api-reference/broadcasts/get-stats-for-a-list-of-broadcasts) ## 🚀 Sequence Emails CRUD API Full CRUD for the individual emails inside a sequence is now available on the V4 API: * `GET /v4/sequences/:sequence_id/emails` — list emails in send order. `content` is omitted by default; pass `include_content=true` to include it. * `GET /v4/sequences/:sequence_id/emails/:id` — fetch a single email, always with full content. * `POST /v4/sequences/:sequence_id/emails` — create an email (`published: false` by default so it doesn't go out until you flip it). * `PUT /v4/sequences/:sequence_id/emails/:id` — update subject, content, timing, send days, position, or publish state. * `DELETE /v4/sequences/:sequence_id/emails/:id` — remove an email; subscribers already queued for it will skip it. Two delay modes: `delay_unit: "days"` follows the sequence schedule, `delay_unit: "hours"` bypasses it and fires once the delay elapses (checked every 15 minutes). Per-email `send_days` overrides the sequence-level schedule (days mode only) — pass `send_days: null` to revert to inheriting it. Snippets are referenced inside `content` via `{{ snippet. }}` and resolve at send time, so editing a snippet automatically updates every email referencing it without re-publishing. Full reference: [/api-reference/sequence-emails/list-sequence-emails](https://developers.kit.com/api-reference/sequence-emails/list-sequence-emails) · [/api-reference/sequence-emails/get-a-sequence-email](https://developers.kit.com/api-reference/sequence-emails/get-a-sequence-email) · [/api-reference/sequence-emails/create-a-sequence-email](https://developers.kit.com/api-reference/sequence-emails/create-a-sequence-email) · [/api-reference/sequence-emails/update-a-sequence-email](https://developers.kit.com/api-reference/sequence-emails/update-a-sequence-email) · [/api-reference/sequence-emails/delete-a-sequence-email](https://developers.kit.com/api-reference/sequence-emails/delete-a-sequence-email) ## 🚀 `GET /v4/subscribers` now supports opt-in enrichment via `include=` `GET /v4/subscribers` now accepts an `include=` query parameter that returns additional fields alongside the base subscriber record. Pass a comma-separated list of any of the following values: * `include=attribution` — referrer, UTM parameters, and Kit source data per subscriber * `include=tags` — array of `{id, name}` tag objects * `include=location` — city, state, country, lat, and lng * `include=canceled_at` — ISO 8601 cancellation timestamp; requires `status=cancelled` (returns `422` otherwise) Values can be combined (e.g. `include=tags,location,canceled_at`). Omitting `include=` leaves existing response shape and performance unchanged. Invalid values return `422` with the full list of accepted options. Full reference: [/api-reference/subscribers/list-subscribers](https://developers.kit.com/api-reference/subscribers/list-subscribers) ## 🚀 Snippets CRUD API Snippets are now available on the V4 API: * `GET /v4/snippets` — list snippets, with `snippet_type` and `archived` filters. `content` and `document` are omitted by default — pass `include_content=true` to include them. * `GET /v4/snippets/:id` — fetch a single snippet (always includes content). * `POST /v4/snippets` — create a snippet. * `PUT /v4/snippets/:id` — rename, update content, or archive/restore (pass `archived: true` / `false` in the body). Snippets come in two types: *inline* snippets store plain text with Liquid variable support in `content`, and *block* snippets store rich HTML in `document_attributes[value_html]`. A snippet's type is set on creation and cannot be changed via `PUT`. Each response includes a `key` field — that's the identifier used to reference a snippet from a broadcast or sequence email via Liquid (`{{ snippet.key }}`). The API also rejects circular references (a snippet nesting itself) with a clear validation error. Full reference: [/api-reference/snippets/list-snippets](https://developers.kit.com/api-reference/snippets/list-snippets) · [/api-reference/snippets/get-a-snippet](https://developers.kit.com/api-reference/snippets/get-a-snippet) · [/api-reference/snippets/create-a-snippet](https://developers.kit.com/api-reference/snippets/create-a-snippet) · [/api-reference/snippets/update-a-snippet](https://developers.kit.com/api-reference/snippets/update-a-snippet) ## 🚀 Help Text Hyperlinks You can now use markdown formatting in the help field of all plugin component library. The following formatting is supported: * Hyperlinks: [link text](https://example.com) * Bold: **bold** * Italic: *italic* [Plugin Component Library](https://developers.kit.com/plugins/component-library/overview) ## 🚀 Newsletter posts on the V4 API Two new read-only endpoints expose newsletter posts on the V4 API — including drafts and web-only posts that weren't reachable through broadcasts: * `GET /v4/posts` — cursor-paginated list (up to 1000 per page). Pass `include_content=true` to return the full HTML body (off by default to keep responses small). * `GET /v4/posts/{id}` — fetch a single post, always includes `content`. Each response includes `id`, `publication_id`, `title`, `slug`, `description`, `status`, `published_at`, `sent_at`, `thumbnail_url`, `is_paid`, and `public_url`. Use `publication_id` to join a post back to its broadcast via `GET /v4/broadcasts`. Supports API key and OAuth2. Full reference: [/api-reference/posts/list-posts](https://developers.kit.com/api-reference/posts/list-posts) · [/api-reference/posts/get-a-post](https://developers.kit.com/api-reference/posts/get-a-post) ## 🚀 Sending addresses on the Account endpoint The `GET /v4/account` response now includes a `sending_addresses` array listing every sending address configured on the account. Each entry contains `email_address`, `from_name`, `status`, `is_default`, `is_verified`, and `is_dmarc_configured`. These addresses can be used when creating or updating any email via the API. Unverified addresses are included and marked with `is_verified: false`. Full reference: [/api-reference/accounts/get-current-account](https://developers.kit.com/api-reference/accounts/get-current-account) ## 🚀 Sequences CRUD API Full CRUD support for sequences is now available on the V4 API. Five endpoints shipped: * `GET /v4/sequences` — expanded to return the full sequence object (13 fields including `send_days`, `time_zone`, `email_address`, `exclude_subscriber_sources`) * `GET /v4/sequences/:id` — fetch a single sequence by ID * `POST /v4/sequences` — create a sequence programmatically * `PUT /v4/sequences/:id` — update sequence settings * `DELETE /v4/sequences/:id` — soft delete with background cleanup Previously, the API only supported listing sequences with minimal fields and managing subscribers. Alongside the soon to be released sequence email endpoints, you can build full sequence management workflows — abandoned checkout flows, upsell journeys, automated templating — without manual intervention in the Kit UI. Docs: [developers.kit.com/api-reference/sequences/list-sequences](https://developers.kit.com/api-reference/sequences/list-sequences) ## 🚀 Broadcast date filtering `GET /v4/broadcasts` now supports `sent_after` and `sent_before` query parameters for server-side date filtering. Use both together to define a date range, or omit both to preserve existing unfiltered behaviour. Filters are applied before pagination, so page sizes and cursor behaviour remain consistent. Invalid date values return `422` with descriptive error messages. This enables incremental sync patterns — instead of fetching your full broadcast history and filtering client-side, call `GET /v4/broadcasts?sent_after=2026-04-16` to retrieve only what's changed since your last run. Full reference: [developers.kit.com/api-reference/broadcasts/list-broadcasts](https://developers.kit.com/api-reference/broadcasts/list-broadcasts) ## 🚀 Default values now supported across all plugin settings components All plugin settings components now support an optional `default` property that pre-fills the setting value when a creator first adds the component. Previously only available on [Radio Group](/plugins/component-library/radio-group), [Slider](/plugins/component-library/slider), [Toggle](/plugins/component-library/toggle), and [Textarea](/plugins/component-library/textarea), it is now also supported on: * [Color Picker](/plugins/component-library/color-picker) (string) * [Text Input](/plugins/component-library/text-input) (string) * [Numerical Input](/plugins/component-library/numerical-input) (number) * [Font Picker](/plugins/component-library/font-picker) (object with `fontFamily` and `fontWeight`) * [Select Input](/plugins/component-library/select-input) (string) * [Date Picker](/plugins/component-library/date-picker) (string, UTC ISO8601) The property is optional and nullable. Existing settings without `default` continue working unchanged. ## 🔧 Going-live checklist prompt added to app submission The Submit for Approval modal now displays a pre-submission checklist when you click "Submit for approval" in the Distribution tab, with a direct link to the [going-live checklist](https://developers.kit.com/kit-app-store/going-live#app-review-checklist). No changes to the submission API or review process. ## 🚀 App Store Search The Kit App Store now has free-text search. Creators can search by app name, category, or description with real-time debounced results and fuzzy/partial matching. The category filter has moved into a dropdown co-located with the search input. No API changes — this is an in-product UI feature available at `app.kit.com` under Automate > Apps. See the [App Store overview](https://developers.kit.com/kit-app-store/overview) and [app details page](https://developers.kit.com/kit-app-store/app-details-page) for context on the App Store surface. ## 🔧 Purchases endpoints now return subscriber\_id The [List Purchases](https://developers.kit.com/api-reference/purchases/list-purchases) and [Get Purchase](https://developers.kit.com/api-reference/purchases/get-a-purchase) endpoints now include `subscriber_id` in the response, eliminating the need for an extra API call to map a purchase to a subscriber. ## 🚀 Kit Developer Docs MCP server Connect your AI coding agent directly to Kit's developer documentation using the [Kit Developer Docs MCP server](https://developers.kit.com/mcp/kit-developer-docs-mcp). Supported clients can query the full API reference on demand, make live API calls on your behalf, and spin up local OAuth servers for testing. ## 🚀 Custom Field Webhooks & Bulk Updates * Introduced 3 new webhook events for custom fields: `custom_field.field_created`, `custom_field.field_deleted`, and `custom_field.field_value_updated`, enabling real-time sync with third-party apps. * Added a bulk update endpoint (`POST /v4/bulk/custom_fields/subscribers`) to update multiple custom field values for multiple subscribers in a single API call. * [Learn more about webhooks](https://developers.kit.com/api-reference/webhooks/create-a-webhook) and [bulk updates](https://developers.kit.com/api-reference/custom-fields/bulk-update-subscriber-custom-field-values). ## 🚀 Transparent color option now available in color picker The [color picker component](https://developers.kit.com/plugins/component-library/color-picker) now supports an `allow_transparent` property that displays a "Transparent" toggle, allowing creators to set colors to transparent. ## 🚀 App Settings now live in Kit App Store Developers can now set an external "App Settings" URL in their app settings, allowing Creators to be able to customize their app setup post-installation, reducing account bloat by controlling data creation and sync. Read about best practices of how to implement this [here](https://developers.kit.com/kit-app-store/app-details-page#how-to-configure). ## 🔧 Improved Kit App Store Sorting * Default sorting now highlights the most popular apps by all-time installations. * Introduced a "Trending" category for apps gaining traction across our creators. * Renamed "Last added" to "Newest" for clarity. ## 🚀 New API endpoints: "List stats for a subscriber" and "Filter subscribers based on engagement" Developers can now use Kit's API to filter subscribers by events like `opened`, `clicked`, `sent`, `delivered`, and `subscribed` with customizable date ranges and event counts. [Explore the API](https://developers.kit.com/api-reference/subscribers/filter-subscribers-based-on-engagement) to enhance subscriber engagement tracking. Additionally, the `List stats` endpoint now supports specifying date ranges for subscriber engagement data. [Learn more](https://developers.kit.com/api-reference/subscribers/list-stats-for-a-subscriber). ## 🚀 Automation nodes app plugin environment launched Developers can now integrate third-party apps with [Kit Visual Automations](https://kit.com/features/automations) using action and event nodes. This opens up powerful new ways for developers to build with Kit, and for creators to automate their workflows. * [Event nodes](https://developers.kit.com/plugins/automation-nodes/plugin-flow#event-node) trigger automations on conditions like "call booked" or "survey completed". * [Action nodes](https://developers.kit.com/plugins/automation-nodes/plugin-flow#action-node) perform tasks in external systems such as "send an SMS" or "enroll a subscriber in a course". * Apps like [Shopify](https://app.kit.com/apps/330), [Thinkific](https://app.kit.com/apps/1451), and [Calendly](https://app.kit.com/apps/2063) are already utilizing these nodes. Explore more in our [documentation](https://developers.kit.com/plugins/automation-nodes/overview). ## 🚀 Dynamic return URLs for app installations Developers can now redirect users back to specific pages after completing an app install using the `return_to` query parameter in [installation flows](https://developers.kit.com/kit-app-store/authentication#externally-initiating-installations), enabling smoother integration experiences that originate from partner sites. ## 📖 Developer changelog now live Stay up to date with Kit's latest developer platform updates through our new changelog featuring: * **Emoji categories**: 🚀 Added, 🔧 Changed, 🐛 Fixed, ⚠️ Breaking Changes * **RSS subscription**: Never miss an update with the RSS feed button * **Smart filtering**: Filter by product area including Kit App Store, Plugins, API, Authentication, and more * **Copy functionality**: Easily share updates with the copy page feature [Subscribe to updates](https://developers.kit.com/changelog/rss.xml) to stay informed about the latest changes.
## 🚀 New subscriber stats endpoint available Get comprehensive engagement metrics for individual subscribers including sends, opens, clicks, bounce rates, and timestamps via the new [subscriber stats API endpoint](https://developers.kit.com/api-reference/subscribers/list-stats-for-a-subscriber). ## 🚀 New plugin components and dependency support now available * New plugin components: [Radio Group](https://developers.kit.com/plugins/component-library/radio-group), [Slider](https://developers.kit.com/plugins/component-library/slider), [Textarea](https://developers.kit.com/plugins/component-library/textarea), [Toggle](https://developers.kit.com/plugins/component-library/toggle), [Numerical Input](https://developers.kit.com/plugins/component-library/numerical-input) * New plugin capabilities: [Group](https://developers.kit.com/plugins/component-library/group), [Dependencies](https://developers.kit.com/plugins/component-library/dependencies) * Plugin enhancements: Transparency and weights in [Font Picker](https://developers.kit.com/plugins/component-library/font-picker), a11y improvements across all components ## 🔧 Kit App Store and app management UX improvements * [Entire app cards](https://app.kit.com/apps) now clickable with streamlined navigation and consistent button hierarchy * New install button directly on [Build tab](https://app.kit.com/apps?is=created) for faster app testing workflows * In app settings (`https://app.kit.com/apps/:app_id/auth`), API and Plugin Authentication separated into distinct sections with clearer plugin type display ## 🚀 App Versioning now available for seamless authentication updates Developers can now ensure creators have access to their latest functionality through [app versioning](https://developers.kit.com/kit-app-store/app-versioning): * Apps automatically create new versions when authentication requirements change * Creators receive smart notifications and can update permissions without reinstalling * Plugin-level scope control with cumulative permission tracking across all plugins ## 🔧 OAuth flow now starts from api.kit.com Kit's OAuth flow [is now initiated](https://developers.kit.com/api-reference/authentication) from `api.kit.com/v4/oauth` for consistency with other API endpoints, replacing the previous `app.kit.com` requirement. ## 🚀 Direct app installation URLs now available Developers can now [drive app installations directly from their websites](https://developers.kit.com/kit-app-store/authentication#externally-initiating-installations) using `https://app.kit.com/apps/:app_id/install?k_app_id=k_:app_id` without requiring users to first visit Kit App Store. ## 🔧 App icons now display consistently across the Kit App Store App thumbnails now display as uniform squares (60x60px on detail pages, 40x40px elsewhere) with proper cropping and centering, ensuring professional appearance regardless of original image dimensions. ## 🚀 Plugin deletion now available for developers Developers can now permanently [delete plugins from their Kit apps](https://developers.kit.com/plugins/managing-plugins#deleting-plugins) to better manage their plugin inventory. ## 🚀 Brand new developer documentation platform We've completely rebuilt our developer documentation from the ground up, centralizing all developer resources in one comprehensive hub at [developers.kit.com](https://developers.kit.com). ### Key improvements include: * **Unified resource center**: All guides, tutorials, and API documentation now live in one carefully structured location * **Updated content**: Every piece of documentation has been refreshed and expanded, from app creation guides to OAuth implementation tutorials * **Enhanced navigation**: Improved site structure makes finding relevant information faster and more intuitive ## 🚀 New interactive features ### API Sandbox * Test Kit's V4 API directly in the documentation * Input your API key and make live requests without leaving the docs * Perfect for rapid prototyping and testing ### Advanced Search & AI Support * Full-text search across all documentation * Built-in "Ask AI" functionality for instant answers * Industry-standard llms.txt and llms-full.txt support for AI integrations ## 🔧 Developer experience improvements * **Dark mode support**: Documentation now adapts to your preferred viewing mode * **SEO optimized**: Better discoverability for developers searching for Kit integration help * **Quick action CTAs**: Streamlined paths to sign up, join the developer community, and contact support * **Mobile responsive**: Optimized experience across all devices