Queue an import of an export document
https://forumulate.com/api /orgs/{orgId}/imports Queue an organization export to be imported by the worker, for a document too large to finish within one request; answers at once with the job to poll.
Takes the same document as POST /orgs/{orgId}/import and answers 202 at once with a job: { id, status: "queued", progress: { done, total } }. The worker imports it with the same rules as the synchronous import (idempotent by slug, additive, authors and poll voters resolved to members or staged accounts). Poll GET /orgs/{orgId}/imports/{id} for progress (one unit per page and per topic) and, when status is succeeded or failed, the summary or error. Use this for anything that would not finish within 30 seconds — a forum of more than a few hundred topics.
Requires token scope: write (the coarse method gate) for Atlas key (ak_), Personal Access Token (cft_) or OAuth (cfo_) auth — in addition to any catalog scope (GET /pats/scopes) the route’s capability needs. Session/cookie auth is governed by RBAC capabilities instead.
Parameters
orgIdstringpathrequiredforumulate-versionstringheaderforumulate-version: 2026-12-01 on every request from a new integration; it is optional only so that integrations written before versioning keep working. Each version includes everything before it:Idempotency-Keystringheader409. The key is scoped to the method and path, so the same key on a different endpoint is a different request. Error responses of 5xx are never cached — those retries re-run. A key is bound to the BODY it was first used with: reusing it with a different body is a 409 idempotency.key_reused, not a replay, because replaying would tell you a write landed that never ran. Generate one key per request, never per process or per session.Request body application/json
formatstringversionintegerexportedAtstringorganizationmembersobject[]Show child parameters
userIdstringrequiredemailstringnamestringavatarUrlstringspacesobject[]Show child parameters
idstringnamestringrequiredslugstringrequiredtypestringrequiredvisibilitystringsettingsobjectcollectionsobject[]Show child parameters
idstringnamestringrequiredslugstringrequiredpagesobject[]Show child parameters
idstringcollectionIdstringrequiredtitlestringrequiredslugstringrequiredcontentmarkdownstringcategoriesobject[]Show child parameters
idstringslugstringrequirednamestringrequireddescriptionstringcolorstringparentstringsortOrderintegerlogoUrlstringtopicsobject[]Show child parameters
idstringtitlestringrequiredslugstringrequiredkindstringstatestringtagsstring[]categoryIdstringcategorystringpinnedbooleanpinnedGloballybooleanfeaturedbooleanlistedbooleanviewCountintegeranswerPostIdstringpollpollsobject[]roadmapStatusstringfieldsobjectunattributedVotesintegercreatedAtstringlastActivityAtstringpostsobject[]{
"format": "string",
"version": 0,
"exportedAt": "string",
"organization": {},
"members": [
{
"userId": "string",
"email": "string",
"name": "string",
"avatarUrl": "string"
}
],
"spaces": [
{
"id": "string",
"name": "string",
"slug": "string",
"type": "string",
"visibility": "string",
"settings": {},
"collections": [
{
"id": null,
"name": null,
"slug": null
}
],
"pages": [
{
"id": null,
"collectionId": null,
"title": null,
"slug": null,
"content": null,
"markdown": null
}
],
"categories": [
{
"id": null,
"slug": null,
"name": null,
"description": null,
"color": null,
"parent": null,
"sortOrder": null,
"logoUrl": null
}
],
"topics": [
{
"id": null,
"title": null,
"slug": null,
"kind": null,
"state": null,
"tags": null,
"categoryId": null,
"category": null,
"pinned": null,
"pinnedGlobally": null,
"featured": null,
"listed": null,
"viewCount": null,
"answerPostId": null,
"poll": null,
"polls": null,
"roadmapStatus": null,
"fields": null,
"unattributedVotes": null,
"createdAt": null,
"lastActivityAt": null,
"posts": null
}
]
}
]
}cURL
curl -X POST "https://forumulate.com/api/orgs/{orgId}/imports" \
-H "forumulate-version: <value>" \
-H "Idempotency-Key: <value>" \
-H "Content-Type: application/json" \
-d '{"format":"string","version":0,"exportedAt":"string","organization":{},"members":[{"userId":"string","email":"string","name":"string","avatarUrl":"string"}],"spaces":[{"id":"string","name":"string","slug":"string","type":"string","visibility":"string","settings":{},"collections":[{"id":null,"name":null,"slug":null}],"pages":[{"id":null,"collectionId":null,"title":null,"slug":null,"content":null,"markdown":null}],"categories":[{"id":null,"slug":null,"name":null,"description":null,"color":null,"parent":null,"sortOrder":null,"logoUrl":null}],"topics":[{"id":null,"title":null,"slug":null,"kind":null,"state":null,"tags":null,"categoryId":null,"category":null,"pinned":null,"pinnedGlobally":null,"featured":null,"listed":null,"viewCount":null,"answerPostId":null,"poll":null,"polls":null,"roadmapStatus":null,"fields":null,"unattributedVotes":null,"createdAt":null,"lastActivityAt":null,"posts":null}]}]}'TypeScript
const res = await fetch('https://forumulate.com/api/orgs/{orgId}/imports', {
method: 'POST',
headers: {
'forumulate-version': '<value>',
'Idempotency-Key': '<value>',
'Content-Type': 'application/json',
},
body: JSON.stringify({"format":"string","version":0,"exportedAt":"string","organization":{},"members":[{"userId":"string","email":"string","name":"string","avatarUrl":"string"}],"spaces":[{"id":"string","name":"string","slug":"string","type":"string","visibility":"string","settings":{},"collections":[{"id":null,"name":null,"slug":null}],"pages":[{"id":null,"collectionId":null,"title":null,"slug":null,"content":null,"markdown":null}],"categories":[{"id":null,"slug":null,"name":null,"description":null,"color":null,"parent":null,"sortOrder":null,"logoUrl":null}],"topics":[{"id":null,"title":null,"slug":null,"kind":null,"state":null,"tags":null,"categoryId":null,"category":null,"pinned":null,"pinnedGlobally":null,"featured":null,"listed":null,"viewCount":null,"answerPostId":null,"poll":null,"polls":null,"roadmapStatus":null,"fields":null,"unattributedVotes":null,"createdAt":null,"lastActivityAt":null,"posts":null}]}]}),
});
const data = await res.json();Python
import requests
res = requests.request(
"POST",
"https://forumulate.com/api/orgs/{orgId}/imports",
headers={
"forumulate-version": "<value>",
"Idempotency-Key": "<value>",
"Content-Type": "application/json",
},
json={"format": "string", "version": 0, "exportedAt": "string", "organization": {}, "members": [{"userId": "string", "email": "string", "name": "string", "avatarUrl": "string"}], "spaces": [{"id": "string", "name": "string", "slug": "string", "type": "string", "visibility": "string", "settings": {}, "collections": [{"id": None, "name": None, "slug": None}], "pages": [{"id": None, "collectionId": None, "title": None, "slug": None, "content": None, "markdown": None}], "categories": [{"id": None, "slug": None, "name": None, "description": None, "color": None, "parent": None, "sortOrder": None, "logoUrl": None}], "topics": [{"id": None, "title": None, "slug": None, "kind": None, "state": None, "tags": None, "categoryId": None, "category": None, "pinned": None, "pinnedGlobally": None, "featured": None, "listed": None, "viewCount": None, "answerPostId": None, "poll": None, "polls": None, "roadmapStatus": None, "fields": None, "unattributedVotes": None, "createdAt": None, "lastActivityAt": None, "posts": None}]}]},
)
data = res.json()Go
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
req, _ := http.NewRequest("POST", "https://forumulate.com/api/orgs/{orgId}/imports", strings.NewReader(`{"format":"string","version":0,"exportedAt":"string","organization":{},"members":[{"userId":"string","email":"string","name":"string","avatarUrl":"string"}],"spaces":[{"id":"string","name":"string","slug":"string","type":"string","visibility":"string","settings":{},"collections":[{"id":null,"name":null,"slug":null}],"pages":[{"id":null,"collectionId":null,"title":null,"slug":null,"content":null,"markdown":null}],"categories":[{"id":null,"slug":null,"name":null,"description":null,"color":null,"parent":null,"sortOrder":null,"logoUrl":null}],"topics":[{"id":null,"title":null,"slug":null,"kind":null,"state":null,"tags":null,"categoryId":null,"category":null,"pinned":null,"pinnedGlobally":null,"featured":null,"listed":null,"viewCount":null,"answerPostId":null,"poll":null,"polls":null,"roadmapStatus":null,"fields":null,"unattributedVotes":null,"createdAt":null,"lastActivityAt":null,"posts":null}]}]}`))
req.Header.Set("forumulate-version", "<value>")
req.Header.Set("Idempotency-Key", "<value>")
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}PHP
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://forumulate.com/api/orgs/{orgId}/imports');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['forumulate-version: <value>', 'Idempotency-Key: <value>', 'Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, '{"format":"string","version":0,"exportedAt":"string","organization":{},"members":[{"userId":"string","email":"string","name":"string","avatarUrl":"string"}],"spaces":[{"id":"string","name":"string","slug":"string","type":"string","visibility":"string","settings":{},"collections":[{"id":null,"name":null,"slug":null}],"pages":[{"id":null,"collectionId":null,"title":null,"slug":null,"content":null,"markdown":null}],"categories":[{"id":null,"slug":null,"name":null,"description":null,"color":null,"parent":null,"sortOrder":null,"logoUrl":null}],"topics":[{"id":null,"title":null,"slug":null,"kind":null,"state":null,"tags":null,"categoryId":null,"category":null,"pinned":null,"pinnedGlobally":null,"featured":null,"listed":null,"viewCount":null,"answerPostId":null,"poll":null,"polls":null,"roadmapStatus":null,"fields":null,"unattributedVotes":null,"createdAt":null,"lastActivityAt":null,"posts":null}]}]}');
$response = curl_exec($ch);
curl_close($ch);
echo $response;Ruby
require 'net/http'
require 'uri'
uri = URI('https://forumulate.com/api/orgs/{orgId}/imports')
req = Net::HTTP::Post.new(uri)
req['forumulate-version'] = '<value>'
req['Idempotency-Key'] = '<value>'
req['Content-Type'] = 'application/json'
req.body = '{"format":"string","version":0,"exportedAt":"string","organization":{},"members":[{"userId":"string","email":"string","name":"string","avatarUrl":"string"}],"spaces":[{"id":"string","name":"string","slug":"string","type":"string","visibility":"string","settings":{},"collections":[{"id":null,"name":null,"slug":null}],"pages":[{"id":null,"collectionId":null,"title":null,"slug":null,"content":null,"markdown":null}],"categories":[{"id":null,"slug":null,"name":null,"description":null,"color":null,"parent":null,"sortOrder":null,"logoUrl":null}],"topics":[{"id":null,"title":null,"slug":null,"kind":null,"state":null,"tags":null,"categoryId":null,"category":null,"pinned":null,"pinnedGlobally":null,"featured":null,"listed":null,"viewCount":null,"answerPostId":null,"poll":null,"polls":null,"roadmapStatus":null,"fields":null,"unattributedVotes":null,"createdAt":null,"lastActivityAt":null,"posts":null}]}]}'
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(req)
end
puts res.bodyJava
import java.net.URI;
import java.net.http.*;
import java.net.http.HttpRequest.BodyPublishers;
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://forumulate.com/api/orgs/{orgId}/imports"))
.header("forumulate-version", "<value>")
.header("Idempotency-Key", "<value>")
.header("Content-Type", "application/json")
.method("POST", BodyPublishers.ofString("{\"format\":\"string\",\"version\":0,\"exportedAt\":\"string\",\"organization\":{},\"members\":[{\"userId\":\"string\",\"email\":\"string\",\"name\":\"string\",\"avatarUrl\":\"string\"}],\"spaces\":[{\"id\":\"string\",\"name\":\"string\",\"slug\":\"string\",\"type\":\"string\",\"visibility\":\"string\",\"settings\":{},\"collections\":[{\"id\":null,\"name\":null,\"slug\":null}],\"pages\":[{\"id\":null,\"collectionId\":null,\"title\":null,\"slug\":null,\"content\":null,\"markdown\":null}],\"categories\":[{\"id\":null,\"slug\":null,\"name\":null,\"description\":null,\"color\":null,\"parent\":null,\"sortOrder\":null,\"logoUrl\":null}],\"topics\":[{\"id\":null,\"title\":null,\"slug\":null,\"kind\":null,\"state\":null,\"tags\":null,\"categoryId\":null,\"category\":null,\"pinned\":null,\"pinnedGlobally\":null,\"featured\":null,\"listed\":null,\"viewCount\":null,\"answerPostId\":null,\"poll\":null,\"polls\":null,\"roadmapStatus\":null,\"fields\":null,\"unattributedVotes\":null,\"createdAt\":null,\"lastActivityAt\":null,\"posts\":null}]}]}"))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());Responses
Example
{
"job": {
"createdAt": "2024-01-01T00:00:00Z",
"error": "string",
"finishedAt": "2024-01-01T00:00:00Z",
"id": "string",
"progress": {
"done": 0,
"total": 0
},
"startedAt": "2024-01-01T00:00:00Z",
"status": "failed",
"summary": {
"authors": {
"email": null,
"member": null,
"name": null,
"staged": null
},
"categoriesCreated": 0,
"collectionsCreated": 0,
"pagesCreated": 0,
"pagesSkipped": 0,
"pollVotesCreated": 0,
"postsCreated": 0,
"spacesCreated": 0,
"spacesReused": 0,
"topicsCreated": 0,
"topicsFailed": 0,
"topicsSkipped": 0,
"warnings": [
null
],
"warningsTruncated": true
}
},
"warnings": [
"tags: \"Launch Day\" is stored as \"launch-day\""
]
}Schema
jobobjectrequiredShow child parameters
createdAtstringrequirederrorstringrequiredfinishedAtstringrequiredidstringrequiredprogressobjectrequiredShow child parameters
donenumberrequiredtotalnumberrequiredstartedAtstringrequiredstatusstringrequiredsummaryImportSummaryrequiredShow child parameters
authorsobjectrequiredcategoriesCreatednumberrequiredcollectionsCreatednumberrequiredpagesCreatednumberrequiredpagesSkippednumberrequiredpollVotesCreatednumberrequiredpostsCreatednumberrequiredspacesCreatednumberrequiredspacesReusednumberrequiredtopicsCreatednumberrequiredtopicsFailednumberrequiredtopicsSkippednumberrequiredwarningsImportWarning[]requiredwarningsTruncatedbooleanwarningsstring[][]. The same sentences are on the X-Field-Warnings response header. If this is present, something you sent is not what is stored; read the resource back rather than assuming your own value.request.malformed). A value that was understood but is not acceptable is a 422 validation.failed.Example
{
"error": {
"code": "string",
"message": "string",
"details": {},
"docs": "string",
"reason": "string",
"slugs": [
"string"
],
"conflict": {
"etag": "string",
"ops": [
null
]
}
},
"requestId": "string"
}Schema
errorobjectrequiredShow child parameters
codestringrequiredrbac.forbidden.messagestringrequireddetailsobjectdocsstringGET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.reasonstringiat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.slugsstring[]space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.conflictobjectPATCH …/entries/{entryId}/fields) whose If-Match was stale.Show child parameters
etagstringrequiredopsarrayrequiredIf-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.requestIdstringx-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.auth.required). Send a Bearer token or the session cookie.Example
{
"error": {
"code": "string",
"message": "string",
"details": {},
"docs": "string",
"reason": "string",
"slugs": [
"string"
],
"conflict": {
"etag": "string",
"ops": [
null
]
}
},
"requestId": "string"
}Schema
errorobjectrequiredShow child parameters
codestringrequiredrbac.forbidden.messagestringrequireddetailsobjectdocsstringGET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.reasonstringiat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.slugsstring[]space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.conflictobjectPATCH …/entries/{entryId}/fields) whose If-Match was stale.Show child parameters
etagstringrequiredopsarrayrequiredIf-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.requestIdstringx-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.rbac.forbidden, whether what is missing is the capability, the token scope, or a resource the token is pinned away from. The message names the capability and, for a scope refusal, the scopes that would grant it. A refusal because of the PLAN is a 402 with quota.exceeded, not this.Example
{
"error": {
"code": "string",
"message": "string",
"details": {},
"docs": "string",
"reason": "string",
"slugs": [
"string"
],
"conflict": {
"etag": "string",
"ops": [
null
]
}
},
"requestId": "string"
}Schema
errorobjectrequiredShow child parameters
codestringrequiredrbac.forbidden.messagestringrequireddetailsobjectdocsstringGET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.reasonstringiat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.slugsstring[]space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.conflictobjectPATCH …/entries/{entryId}/fields) whose If-Match was stale.Show child parameters
etagstringrequiredopsarrayrequiredIf-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.requestIdstringx-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.*.not_found).Example
{
"error": {
"code": "string",
"message": "string",
"details": {},
"docs": "string",
"reason": "string",
"slugs": [
"string"
],
"conflict": {
"etag": "string",
"ops": [
null
]
}
},
"requestId": "string"
}Schema
errorobjectrequiredShow child parameters
codestringrequiredrbac.forbidden.messagestringrequireddetailsobjectdocsstringGET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.reasonstringiat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.slugsstring[]space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.conflictobjectPATCH …/entries/{entryId}/fields) whose If-Match was stale.Show child parameters
etagstringrequiredopsarrayrequiredIf-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.requestIdstringx-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.validation.failed). error.details names the field paths.Example
{
"error": {
"code": "string",
"message": "string",
"details": {},
"docs": "string",
"reason": "string",
"slugs": [
"string"
],
"conflict": {
"etag": "string",
"ops": [
null
]
}
},
"requestId": "string"
}Schema
errorobjectrequiredShow child parameters
codestringrequiredrbac.forbidden.messagestringrequireddetailsobjectdocsstringGET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.reasonstringiat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.slugsstring[]space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.conflictobjectPATCH …/entries/{entryId}/fields) whose If-Match was stale.Show child parameters
etagstringrequiredopsarrayrequiredIf-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.requestIdstringx-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.Example
{
"error": {
"code": "string",
"message": "string",
"details": {},
"docs": "string",
"reason": "string",
"slugs": [
"string"
],
"conflict": {
"etag": "string",
"ops": [
null
]
}
},
"requestId": "string"
}Schema
errorobjectrequiredShow child parameters
codestringrequiredrbac.forbidden.messagestringrequireddetailsobjectdocsstringGET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.reasonstringiat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.slugsstring[]space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.conflictobjectPATCH …/entries/{entryId}/fields) whose If-Match was stale.Show child parameters
etagstringrequiredopsarrayrequiredIf-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.requestIdstringx-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.Example
{
"error": {
"code": "string",
"message": "string",
"details": {},
"docs": "string",
"reason": "string",
"slugs": [
"string"
],
"conflict": {
"etag": "string",
"ops": [
null
]
}
},
"requestId": "string"
}Schema
errorobjectrequiredShow child parameters
codestringrequiredrbac.forbidden.messagestringrequireddetailsobjectdocsstringGET /space-types/{type}/presets. Present on errors over a controlled list, so the answer to "what am I allowed to send?" is in the error itself.reasonstringiat_too_old, iat_in_future, bad_signature, bad_issuer, bad_audience, expired and more. Branch on this, not on message.slugsstring[]space.slug_taken, page.slug_taken, collection.slug_taken, site.slug_taken or badge.slug_taken.conflictobjectPATCH …/entries/{entryId}/fields) whose If-Match was stale.Show child parameters
etagstringrequiredopsarrayrequiredIf-Match that touch a path your own operations touch. Empty: none did — retry the batch unchanged under etag. null: the history no longer reaches your tag — re-read the entry first.requestIdstringx-request-id and x-forumulate-request-id response headers. Quote it in a bug report: it is the only thing that finds your exact request in our logs. It is in the body because that is the part that survives — an SDK that throws the parsed body, a serverless log that keeps the body and drops the headers, a ticket with the JSON pasted in.