Create, retrieve, list, and update Web and In-App forms with the polymorphic form definition.
OpenAPI Spec
Machine-readable spec for the Forms API at revision 2026-10-15.pre: beta/categories/forms.json
Before you begin
Check out our general API overview to make sure you're ready to get started with specific endpoints.
Beta
The polymorphic form definition is in beta. Send the
revision: 2026-10-15.preheader to use it. Endpoints may change before general availability. The generally available Web form contracts at2026-10-15and earlier don't change. See the Forms API overview for those.
At revision 2026-10-15.pre, the Forms API supports two kinds of forms:
- Web forms appear on your website.
- In-App forms appear inside your mobile app.
Both kinds share one form definition shape. You choose the kind with the type field on the definition.
Breaking change for beta callers
If you already call the Forms API with a
2026-10-15.prerevision header, thedefinitionattribute has a new shape. You must wrap your existingversionsarray in an object that has atypeofweb. See Move an existing Web form integration to the beta shape.
Supported operations
| Operation | Endpoint | Web forms | In-App forms |
|---|---|---|---|
| Create Form | POST /api/forms/ | Yes | Yes (draft only) |
| Get Form | GET /api/forms/{id} | Yes | Yes |
| Get Forms | GET /api/forms/ | Yes | Yes |
| Update Form | PATCH /api/forms/{id} | Yes | Yes (draft versions only) |
| Publish Form | POST /api/form-publish-jobs | Yes | Yes |
| Unpublish Form | POST /api/form-unpublish-jobs | Yes | Yes |
Keep the following limitations in mind:
- Publish Form and Unpublish Form don't return a job that you can retrieve. To confirm the result, call Get Form and check the form's
status. See Publish and unpublish a form. - You can't convert a form from one type to the other. A form keeps the
typeit was created with. - In-App forms don't support A/B tests. You can't create or edit an In-App A/B test with the API.
- Create Form accepts exactly one version and one step for In-App forms.
Data model
A form has the following:
-
idThe form ID.
-
attributes-
nameThe name of the form.
-
statusThe current status of the form, e.g.,
draftorlive. -
ab_testWhether or not the form has an A/B test. This is always
falsefor In-App forms. -
created_atThe timestamp of when the form was created.
-
updated_atThe timestamp of when the form was last updated.
-
definitionA definition of the form. It has the following fields:
-
typeThe kind of form:
weborin-app. This value decides which fields each entry inversionsaccepts. -
versionsThe versions of the form. Each version holds the form's layout, content, triggers, and targeting.
-
-
Web form definitions
When type is web, each entry in versions has the same fields as the definition.versions entries in the Forms API overview. Those fields are unchanged. The only difference is the type field next to versions.
ab_test stays a top-level attribute of the form for Web forms.
In-App form definitions
When type is in-app, each entry in versions has the following fields:
-
form_typeThe layout of the form:
popup,fullscreen, orflyout. -
locationThe position of a flyout:
top_centerorbottom_center. Required whenform_typeisflyout. Not allowed for other layouts. -
stepsThe screens of the form. Each step has
columns, each column hasrows, and each row hasblocks. -
triggersWhen and how often the form appears. See Triggers.
-
targetingOptional. The audience for the version, as an
allow_listordeny_listof list and segment IDs. If you includetargeting, it must name at least one list or segment. If you omit it, the form can show to all profiles with the mobile app. -
stylesOptional. Styling for the form container, such as
width,background_color, andpadding. If you omit it, Klaviyo applies defaults.
Targeting belongs to each version, not to the definition.
Klaviyo generates the id of every version, step, column, row, and block. Don't send these IDs, or the name, status, or ab_test of a version, when you create a form. Create Form returns them in the response.
Blocks
Each block has a type. In-App forms support the following block types:
| Block type | Description |
|---|---|
text | Text with basic HTML formatting. |
image | An image from a src URL or a Klaviyo image library asset_id. It can have an optional action. |
button | A button with a label and an action. |
coupon | A static coupon code. |
countdown_timer | A countdown to a fixed end time, or for a fixed duration. |
review | A review that you supply. The review isn't looked up from your account. |
custom_html | Custom HTML. Your account must be allowed to use custom CSS and HTML. |
A button's action has a type of close_form, deeplink_to_screen, or open_url:
- For
deeplink_to_screen, senddeeplink_url. To send each platform to a different screen, setuse_separate_deeplink_urlstotrueand send bothdeeplink_url_iosanddeeplink_url_androidinstead. - For
open_url, sendweb_url. The URL scheme must behttp,https,mailto,tel, orsms.
Triggers
The triggers object controls when the form appears:
-
display_triggerWhat causes the form to appear. Set its
typeto one of the following:app_open: The form appears when a profile opens your app.performed_event: The form appears when a profile performs an event. Send theevent_name. You can also send one entry inproperty_filtersto match an event property exactly.push_notification_open: The form appears when a profile opens a push notification. Send themessage_titleto match.
-
delay_secondsHow long to wait after the trigger, in seconds, up to 86400. The default is
0. -
timeout_daysThe minimum number of days before the form shows to the same profile again. The default is
7. -
suppress_after_dismissWhether to stop showing the form to a profile after they dismiss it. The default is
true.
Create a form
Create a draft form with the Create Form endpoint. POST /api/forms/
All forms are created in a draft status. The status attribute is optional and defaults to draft, so you can omit it. Sending live returns a 400 error. Send a definition with a type of web or in-app. A successful request returns a 201 and the form's definition, including the IDs that Klaviyo generated.
Create an In-App form
The following request creates an In-App popup with a text block and a button. The popup appears 5 seconds after a profile opens your app.
curl --request POST \
--url https://a.klaviyo.com/api/forms/ \
--header 'Authorization: Klaviyo-API-Key your-private-api-key' \
--header 'accept: application/vnd.api+json' \
--header 'content-type: application/vnd.api+json' \
--header 'revision: 2026-10-15.pre' \
--data '
{
"data": {
"type": "form",
"attributes": {
"name": "Welcome offer",
"definition": {
"type": "in-app",
"versions": [
{
"form_type": "popup",
"triggers": {
"display_trigger": {
"type": "app_open"
},
"delay_seconds": 5,
"timeout_days": 7,
"suppress_after_dismiss": true
},
"steps": [
{
"columns": [
{
"rows": [
{
"blocks": [
{
"type": "text",
"content": "<p>Get 10% off your first order</p>"
}
]
},
{
"blocks": [
{
"type": "button",
"label": "Shop now",
"action": {
"type": "deeplink_to_screen",
"deeplink_url": "myapp://screen/shop"
}
}
]
}
]
}
]
}
]
}
]
}
}
}
}
'
{
"data": {
"type": "form",
"id": "Ser5Vq",
"attributes": {
"status": "draft",
"ab_test": false,
"name": "Welcome offer",
"created_at": "2026-10-22T15:50:35.794587+00:00",
"updated_at": "2026-10-22T15:50:35.794606+00:00",
"definition": {
"type": "in-app",
"versions": [
{
"form_type": "popup",
"triggers": {
"display_trigger": {
"type": "app_open"
},
"delay_seconds": 5,
"timeout_days": 7,
"suppress_after_dismiss": true
},
"steps": [
{
"columns": [
{
"rows": [
{
"blocks": [
{
"type": "text",
"content": "<p>Get 10% off your first order</p>",
"styles": {
"padding": {
"left": 6,
"right": 6,
"top": 10,
"bottom": 10
},
"background_color": null
},
"id": "01M5J4X71KGDNK3R34WVM2ASKF"
}
],
"id": "01M5J4X71G2B9E9XEZH6PNGFW9"
},
{
"blocks": [
{
"type": "button",
"label": "Shop now",
"action": {
"type": "deeplink_to_screen",
"deeplink_url": "myapp://screen/shop",
"deeplink_url_ios": null,
"deeplink_url_android": null,
"use_separate_deeplink_urls": null,
"web_url": null,
"id": "01M5J4X7207AX231JGK29DN5W1"
},
"styles": {
"background_color": null,
"padding": {
"left": 6,
"right": 6,
"top": 10,
"bottom": 10
},
"inner_padding": null,
"text_styles": {
"font_family": "Arial, 'Helvetica Neue', Helvetica, sans-serif",
"font_size": 16,
"font_weight": 700,
"text_color": "#FFFFFF",
"character_spacing": 0,
"font_style": null,
"text_decoration": null
},
"color": null,
"height": 50,
"width": "fill",
"border_styles": {
"radius": 2,
"color": null,
"style": null,
"thickness": null
},
"alignment": "center",
"drop_shadow": {
"enabled": false,
"color": "#000000",
"blur": 15,
"x_offset": 0,
"y_offset": 0
}
},
"id": "01M5J4X71X5YBRP8VFRKD27RVW"
}
],
"id": "01M5J4X71T3XMX546HRB6SGCXW"
}
],
"id": "01M5J4X71BRNERMH6R5QSVJNG0"
}
],
"id": "01M5J4X715NF91ECHP9H6Z8S3S",
"name": "Main"
}
],
"location": null,
"targeting": null,
"styles": {
"width": "medium",
"custom_width": null,
"minimum_height": 250,
"background_color": "#FFFFFF",
"background_image": null,
"overlay_color": "rgba(20,20,20,0.6)",
"drop_shadow": {
"enabled": false,
"blur": 30,
"color": "rgba(0,0,0,0.15)"
},
"border_styles": {
"radius": 2,
"color": "#000000",
"style": null,
"thickness": 0
},
"padding": {
"left": 20,
"right": 20,
"top": 20,
"bottom": 20
},
"margin": {
"left": 0,
"right": 0,
"top": 0,
"bottom": 0
},
"close_button": {
"background_color": "rgba(180, 187, 195, 0.65)",
"outline_color": "#FFFFFF",
"color": "#FFFFFF",
"stroke": 2.0,
"size": 20,
"margin": {
"left": 8,
"right": 8,
"top": 8,
"bottom": 8
}
},
"custom_css": {
"enabled": false,
"css": ""
}
},
"ab_test": false,
"name": "Default",
"id": 29155010,
"status": "draft"
}
]
}
},
"links": {
"self": "https://a.klaviyo.com/api/forms/Ser5Vq/"
}
},
"links": {
"self": "https://a.klaviyo.com/api/forms/"
}
}
Keep the following in mind when you create an In-App form:
- Send exactly one version with exactly one step.
- Send your content in
steps[].columns. Acolumnsshorthand directly on the version isn't supported. - Send
locationfor aflyout, and omit it for other layouts. Thefullwidth style is only supported for aflyout. - Send
ab_testasfalseor omit it, both on the form and on the version. - A static coupon
textand a fixed countdownend_timecan benullin a draft. A fixed countdownend_timemust be in the future when you send it. Image blocks must have a complete image. - Klaviyo names a new version
Defaultand a new stepMain. You can't set names when you create a form. - If a request is invalid, the
400error'ssource.pointerpoints to the field to fix, for example/data/attributes/definition/versions/0/location.
Create a Web form
To create a Web form, set the definition's type to web and put your existing versions next to it. Keep ab_test and status as top-level attributes. The following request creates a simple sign-up form with a sign-up step and a success step. A Web form version needs at least two steps. For a complete Web form body, see Create a form in the Forms API overview.
curl --request POST \
--url https://a.klaviyo.com/api/forms/ \
--header 'Authorization: Klaviyo-API-Key your-private-api-key' \
--header 'accept: application/vnd.api+json' \
--header 'content-type: application/vnd.api+json' \
--header 'revision: 2026-10-15.pre' \
--data '
{
"data": {
"type": "form",
"attributes": {
"name": "My first API form",
"status": "draft",
"ab_test": false,
"definition": {
"type": "web",
"versions": [
{
"steps": [
{
"columns": [
{
"rows": [
{
"blocks": [
{
"type": "html_text",
"properties": {
"content": "<div>Sign up for our newsletter</div>"
}
}
]
},
{
"blocks": [
{
"type": "email",
"properties": {
"label": "Email",
"placeholder": "Enter your email address",
"required": true
}
}
]
},
{
"blocks": [
{
"type": "button",
"properties": {
"label": "Sign up"
},
"action": {
"submit": true,
"type": "next_step",
"properties": {
"list_id": "{SOME_LIST_ID}"
}
}
}
]
}
]
}
],
"name": "email sign-up"
},
{
"columns": [
{
"rows": [
{
"blocks": [
{
"type": "html_text",
"properties": {
"content": "<div>Thank you for subscribing!</div>"
}
}
]
}
]
}
],
"name": "success step"
}
]
}
]
}
}
}
}
'
Move an existing Web form integration to the beta shape
If your integration already creates or reads Web forms at a 2026-10-15.pre revision, make the following changes:
- Add a
typeofwebtodefinition, next toversions. Thedefinitionobject you send has two keys,typeandversions, whereversionsholds the same array you sent before. - Read
definition.typeanddefinition.versionsfrom responses. - Leave
ab_testandstatuswhere they are, as top-level attributes of the form.
Requests at 2026-10-15 and earlier revisions keep their existing shapes.
Get a form
Retrieve a form with the Get Form endpoint. GET /api/forms/{id}
The response has the same shape as the Create Form response, with definition.type set to web or in-app.
For a Web form, definition.type is web and versions is unchanged from the Forms API overview. The following response is trimmed to a single text block to show the new type field. A real response includes all of the version's default properties and styles.
{
"data": {
"type": "form",
"id": "Ser5Vq",
"attributes": {
"status": "draft",
"ab_test": false,
"name": "My first API form",
"created_at": "2026-10-22T15:59:04+00:00",
"updated_at": "2026-10-22T15:59:04+00:00",
"definition": {
"type": "web",
"versions": [
{
"id": 29155217,
"type": "popup",
"channel": "WEB",
"status": "draft",
"ab_test": false,
"steps": [
{
"id": "01JTP6F3EERYR2T30C4ZDCS6JA",
"columns": [
{
"id": "01JTP6F3EQ9TQ6TEWQSV33JDQ7",
"rows": [
{
"id": "01JTP6F3EZRM5M0HM42NPC6QA5",
"blocks": [
{
"id": "01JTP6F3F52N3WXF6MNNSH1MQ4",
"type": "html_text",
"properties": {
"content": "<div>Sign up for our newsletter</div>"
}
}
]
}
]
}
]
}
]
}
]
}
},
"links": {
"self": "https://a.klaviyo.com/api/forms/Ser5Vq"
}
}
}
Get forms
List the forms in your account with the Get Forms endpoint. GET /api/forms/
At this revision, the list includes both Web and In-App forms, each with its definition. To list only one kind, filter on channel_type:
curl --request GET \
--url 'https://a.klaviyo.com/api/forms/?filter=equals(channel_type,"in-app")' \
--header 'Authorization: Klaviyo-API-Key your-private-api-key' \
--header 'accept: application/vnd.api+json' \
--header 'revision: 2026-10-15.pre'
The channel_type filter accepts web or in-app with the equals operator. Any other value returns a 400 error.
Update a form
Edit a form's draft with the Update Form endpoint. PATCH /api/forms/{id}
You can update a form's name and definition. Attributes you omit keep their current values. Neither can be null. Any other attribute, such as status or ab_test, returns a 400 error that points to the field.
When you send a definition, it replaces the editable definition entirely, so use the same workflow as the Forms API overview:
- Retrieve the form with
GET /api/forms/{id}. - Modify the draft versions in the returned definition. Keep every version and its
id. - Send the full definition in your Update Form request.
The following rules apply to updates:
- The definition's
typecan't change, for either Web or In-App forms. Send the sametypethat Get Form returned. Changing it returns a400error that points todefinition/type. - Send each existing version
idexactly once. You can't add or remove versions. If a live form has aliveand adraftversion, send both. - You can only edit draft versions. Return each live version exactly as you received it. Changing a live version returns a
400error. - Anything you leave out of a draft version, such as a block or a row, is deleted from the form. This applies to both Web and In-App forms.
- Update Form doesn't check whether the form changed since you retrieved it. If you send a definition from an earlier Get Form call, it can overwrite edits made in the meantime. Retrieve the form again right before you update it.
The following request renames a form and leaves its definition as it is:
curl --request PATCH \
--url https://a.klaviyo.com/api/forms/Ser5Vq \
--header 'Authorization: Klaviyo-API-Key your-private-api-key' \
--header 'accept: application/vnd.api+json' \
--header 'content-type: application/vnd.api+json' \
--header 'revision: 2026-10-15.pre' \
--data '
{
"data": {
"type": "form",
"id": "Ser5Vq",
"attributes": {
"name": "Welcome offer (renamed)"
}
}
}
'
The following request changes the text of the In-App popup from Create an In-App form to "Get 15% off your first order". It sends the whole definition, with the version, step, column, row, and block IDs that Get Form returned, and with only the text block's content changed:
curl --request PATCH \
--url https://a.klaviyo.com/api/forms/Ser5Vq \
--header 'Authorization: Klaviyo-API-Key your-private-api-key' \
--header 'accept: application/vnd.api+json' \
--header 'content-type: application/vnd.api+json' \
--header 'revision: 2026-10-15.pre' \
--data '
{
"data": {
"type": "form",
"id": "Ser5Vq",
"attributes": {
"definition": {
"type": "in-app",
"versions": [
{
"form_type": "popup",
"triggers": {
"display_trigger": {
"type": "app_open"
},
"delay_seconds": 5,
"timeout_days": 7,
"suppress_after_dismiss": true
},
"steps": [
{
"columns": [
{
"rows": [
{
"blocks": [
{
"type": "text",
"content": "<p>Get 15% off your first order</p>",
"styles": {
"padding": {
"left": 6,
"right": 6,
"top": 10,
"bottom": 10
},
"background_color": null
},
"id": "01M5J4X71KGDNK3R34WVM2ASKF"
}
],
"id": "01M5J4X71G2B9E9XEZH6PNGFW9"
},
{
"blocks": [
{
"type": "button",
"label": "Shop now",
"action": {
"type": "deeplink_to_screen",
"deeplink_url": "myapp://screen/shop",
"deeplink_url_ios": null,
"deeplink_url_android": null,
"use_separate_deeplink_urls": null,
"web_url": null,
"id": "01M5J4X7207AX231JGK29DN5W1"
},
"styles": {
"background_color": null,
"padding": {
"left": 6,
"right": 6,
"top": 10,
"bottom": 10
},
"inner_padding": null,
"text_styles": {
"font_family": "Arial, 'Helvetica Neue', Helvetica, sans-serif",
"font_size": 16,
"font_weight": 700,
"text_color": "#FFFFFF",
"character_spacing": 0,
"font_style": null,
"text_decoration": null
},
"color": null,
"height": 50,
"width": "fill",
"border_styles": {
"radius": 2,
"color": null,
"style": null,
"thickness": null
},
"alignment": "center",
"drop_shadow": {
"enabled": false,
"color": "#000000",
"blur": 15,
"x_offset": 0,
"y_offset": 0
}
},
"id": "01M5J4X71X5YBRP8VFRKD27RVW"
}
],
"id": "01M5J4X71T3XMX546HRB6SGCXW"
}
],
"id": "01M5J4X71BRNERMH6R5QSVJNG0"
}
],
"id": "01M5J4X715NF91ECHP9H6Z8S3S",
"name": "Main"
}
],
"location": null,
"targeting": null,
"styles": {
"width": "medium",
"custom_width": null,
"minimum_height": 250,
"background_color": "#FFFFFF",
"background_image": null,
"overlay_color": "rgba(20,20,20,0.6)",
"drop_shadow": {
"enabled": false,
"blur": 30,
"color": "rgba(0,0,0,0.15)"
},
"border_styles": {
"radius": 2,
"color": "#000000",
"style": null,
"thickness": 0
},
"padding": {
"left": 20,
"right": 20,
"top": 20,
"bottom": 20
},
"margin": {
"left": 0,
"right": 0,
"top": 0,
"bottom": 0
},
"close_button": {
"background_color": "rgba(180, 187, 195, 0.65)",
"outline_color": "#FFFFFF",
"color": "#FFFFFF",
"stroke": 2.0,
"size": 20,
"margin": {
"left": 8,
"right": 8,
"top": 8,
"bottom": 8
}
},
"custom_css": {
"enabled": false,
"css": ""
}
},
"ab_test": false,
"name": "Default",
"id": 29155010,
"status": "draft"
}
]
}
}
}
}
'
A successful request returns a 200 and the updated form, in the same shape as the Get Form response.
Your changes stay in draft until you publish the form.
Publish and unpublish a form
Publish a form's draft version with the Publish Form endpoint, and take a live form down with the Unpublish Form endpoint. Both endpoints work for Web and In-App forms. Their request and response behavior is described in the Forms API overview.
curl --request POST \
--url https://a.klaviyo.com/api/form-publish-jobs/ \
--header 'Authorization: Klaviyo-API-Key your-private-api-key' \
--header 'accept: application/vnd.api+json' \
--header 'content-type: application/vnd.api+json' \
--header 'revision: 2026-10-15.pre' \
--data '
{
"data": {
"type": "form-publish-job",
"relationships": {
"form": {
"data": {
"type": "form",
"id": "Ser5Vq"
}
}
}
}
}
'
To unpublish, send the same request body to POST /api/form-unpublish-jobs/ with the type form-unpublish-job.
Keep the following in mind for In-App forms:
- After you publish an In-App form, Get Form returns two versions: the
liveversion and adraftversion. You edit the draft version, and the live version stays unchanged until you publish again. - After you unpublish, the form returns to
draftand only the draft version remains. - Unpublishing a form that isn't live succeeds without making changes, so you can safely retry it.
- The response contains a job ID, but you can't retrieve the job. Call Get Form to confirm the form's
status.
Next steps
- Create a form with the Create Form endpoint.
- Make a call to Get Form to retrieve the newly created form.
- Edit the draft with Update Form.
- Make the form live with Publish Form, or take it down with Unpublish Form.