API reference
Push records
Push a batch of records into the open sync, or check one with ?validate=1.
Generated from /openapi.json.
Request
POSThttps://www.postaged.com/api/integrations/push/v1/records
Pushes a batch into the open sync. One bad record never stops the others: the response counts what was accepted and lists one rejection per refused record. Values dropped from accepted records are grouped by field and code, each group with a count and up to 5 example record ids; every dropped value is listed one per record by List sync rejections with type=warning. With ?validate=1 the batch is checked against the same rules and nothing is written; a dry run needs no open sync and no Idempotency-Key. A body over the platform's own cap can get the platform's 413 without the documented error body.
curl https://www.postaged.com/api/integrations/push/v1/records \
-H "Authorization: Bearer $POSTAGED_API_KEY" \
-H "Idempotency-Key: 2026-10-03-records-1" \
-H "Content-Type: application/json" \
-d '{
"records": [
{
"external_id": "crm-1042",
"fields": {
"email": "dana@example.com",
"deal_value": "1500"
}
},
{
"external_id": "crm-0977",
"op": "delete"
}
]
}'const response = await fetch("https://www.postaged.com/api/integrations/push/v1/records", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POSTAGED_API_KEY}`,
"Idempotency-Key": "2026-10-03-records-1",
"Content-Type": "application/json",
},
body: JSON.stringify({
records: [
{
external_id: "crm-1042",
fields: {
email: "dana@example.com",
deal_value: "1500",
},
},
{
external_id: "crm-0977",
op: "delete",
},
],
}),
});
const result = await response.json();
console.log(result);import os
import requests
response = requests.post(
"https://www.postaged.com/api/integrations/push/v1/records",
headers={
"Authorization": f"Bearer {os.environ['POSTAGED_API_KEY']}",
"Idempotency-Key": "2026-10-03-records-1",
},
json={
"records": [
{
"external_id": "crm-1042",
"fields": {
"email": "dana@example.com",
"deal_value": "1500",
},
},
{
"external_id": "crm-0977",
"op": "delete",
},
],
},
timeout=120,
)
print(response.json())Authentication
An integration key (it starts with pk_int_), created with New key on the integration's page. Send it in the Authorization: Bearer header. Only that integration's key works here; any other key gets 401 invalid_key.
Authorization: Bearer $POSTAGED_API_KEY
Content-Type: application/jsonParameters
- validatestring (query)OptionalSend 1 to check the batch and write nothing. One of
1. - Idempotency-Keystring (header)OptionalRequired on a push; not read on a ?validate=1 dry run. A new value per request, the same value on a retry of that request. Up to 200 characters.
Request body
- recordsarray of objectsRequiredUp to 1,000 items.
- external_idstringRequiredYour system's stable id for the contact. Up to 128 characters.
- opstringOptionalOne of
upsertordelete. Defaultupsert. - fieldsobjectOptionalValues by declared field key. A null clears nothing; it is sent as blank.
{
"records": [
{
"external_id": "crm-1042",
"fields": { "email": "dana@example.com", "deal_value": "1500" }
},
{ "external_id": "crm-0977", "op": "delete" }
]
}Check a batch with ?validate=1
Add ?validate=1 to check a batch against the same rules and write nothing. A dry run needs no open sync and no Idempotency-Key, and it answers with the same rejections and warnings a push would.
curl "https://www.postaged.com/api/integrations/push/v1/records?validate=1" \
-H "Authorization: Bearer $POSTAGED_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"records": [
{
"external_id": "crm-1042",
"fields": {
"email": "dana@example.com",
"deal_value": "1500"
}
},
{
"external_id": "crm-0977",
"op": "delete"
}
]
}'const response = await fetch("https://www.postaged.com/api/integrations/push/v1/records?validate=1", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POSTAGED_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
records: [
{
external_id: "crm-1042",
fields: {
email: "dana@example.com",
deal_value: "1500",
},
},
{
external_id: "crm-0977",
op: "delete",
},
],
}),
});
const result = await response.json();
console.log(result);import os
import requests
response = requests.post(
"https://www.postaged.com/api/integrations/push/v1/records?validate=1",
headers={"Authorization": f"Bearer {os.environ['POSTAGED_API_KEY']}"},
json={
"records": [
{
"external_id": "crm-1042",
"fields": {
"email": "dana@example.com",
"deal_value": "1500",
},
},
{
"external_id": "crm-0977",
"op": "delete",
},
],
},
timeout=120,
)
print(response.json())Response 200
The batch was checked (and, without ?validate=1, written).
A push returns:
- acceptedinteger
- deletedinteger
- rejectedinteger
- rejectionsarray of objects
- indexintegerThe record's position in the batch.
- externalIdstring or null
- codestringOne of
missing_external_id,invalid_external_id,unknown_field,invalid_type,invalid_email,duplicate_in_batch, orrecord_too_large. - fieldstring
- messagestring
- warningsarray of objects
- indexintegerThe batch position of the group's first record.
- externalIdstringThe group's first record's external_id.
- codestringOne of
invalid_type. - fieldstring
- messagestring
- countintegerHow many accepted records lost a value for this field.
- externalIdsarray of stringsUp to 5 of those records' external_id values, in batch order. Up to 5 items.
A ?validate=1 dry run returns:
- validatedbooleanAlways
true. - acceptedinteger
- rejectedinteger
- rejectionsarray of objects
- indexintegerThe record's position in the batch.
- externalIdstring or null
- codestringOne of
missing_external_id,invalid_external_id,unknown_field,invalid_type,invalid_email,duplicate_in_batch, orrecord_too_large. - fieldstring
- messagestring
- warningsarray of objects
- indexintegerThe batch position of the group's first record.
- externalIdstringThe group's first record's external_id.
- codestringOne of
invalid_type. - fieldstring
- messagestring
- countintegerHow many accepted records lost a value for this field.
- externalIdsarray of stringsUp to 5 of those records' external_id values, in batch order. Up to 5 items.
{
"accepted": 1,
"deleted": 1,
"rejected": 1,
"rejections": [
{
"index": 2,
"externalId": null,
"code": "missing_external_id",
"message": "Every record needs an external_id: it is how this record is recognized on the next sync."
}
],
"warnings": [
{
"index": 0,
"externalId": "crm-1042",
"code": "invalid_type",
"field": "deal_value",
"message": "\"deal_value\" is declared as number and this value is not one. The record was accepted without it.",
"count": 1,
"externalIds": ["crm-1042"]
}
]
}{
"validated": true,
"accepted": 2,
"rejected": 0,
"rejections": [],
"warnings": []
}Errors
Errors use the body { "error": { "code", "message", "details" } }; details appears only on some codes. Full tables: Error reference.
A body over 4.25 MB can get the hosting platform's own 413, without the push API's error body.
| Status | code | message | What to do |
|---|---|---|---|
| 400 | invalid_json | The request body must be JSON. | Send the body as JSON with a Content-Type of application/json. |
| 400 | invalid_request | Send { records: [...] }. | Send the body in the shape the message names, then send the request again. |
| 400 | missing_idempotency_key | Send an Idempotency-Key header so a retried batch is not applied twice. | Send an Idempotency-Key header with a new value for each request, and the same value when you retry that request. |
| 401 | invalid_key | That API key is not valid for this integration. | Send this integration's key (it starts with pk_int_) in the Authorization: Bearer header. A revoked key, another kind of key, or a key for a removed integration gets this error. |
| 403 | integration_unavailable | This integration is not accepting data. Contact support if you think that is wrong. | The integration is paused or disconnected in Postaged. Ask the owner to resume it, then send the request again. |
| 409 | no_open_sync | Begin a sync before pushing records, so a full sync knows what it saw. | Begin a sync with POST /api/integrations/push/v1/sync first. A sync left open expires after 24 hours. |
| 409 | idempotency_key_reused | That Idempotency-Key was used for a different batch. Use a new key. | Use a new Idempotency-Key for this request. Reuse a key only to retry the same request with the same body. |
| 409 | in_progress | That batch is still being processed. | The first attempt is still running. Wait a few seconds, then retry with the same Idempotency-Key to get its response. |
| 413 | too_many_records | You sent 1001 records. Send at most 1000 per batch. | Split the batch so each one holds at most the records in details.limit. |
| 413 | payload_too_large | A batch may be at most 4456448 bytes. | Split the batch so each body stays at or under the bytes in details.limit. |
| 429 | rate_limited | Too many requests. Slow down and retry. | Wait the number of seconds in the Retry-After header, then send the request again. |
{
"error": {
"code": "invalid_json",
"message": "The request body must be JSON."
}
}{
"error": { "code": "invalid_request", "message": "Send { records: [...] }." }
}{
"error": {
"code": "missing_idempotency_key",
"message": "Send an Idempotency-Key header so a retried batch is not applied twice."
}
}HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="Postaged"
{
"error": {
"code": "invalid_key",
"message": "That API key is not valid for this integration."
}
}{
"error": {
"code": "integration_unavailable",
"message": "This integration is not accepting data. Contact support if you think that is wrong."
}
}{
"error": {
"code": "no_open_sync",
"message": "Begin a sync before pushing records, so a full sync knows what it saw."
}
}{
"error": {
"code": "idempotency_key_reused",
"message": "That Idempotency-Key was used for a different batch. Use a new key."
}
}{
"error": {
"code": "in_progress",
"message": "That batch is still being processed."
}
}{
"error": {
"code": "too_many_records",
"message": "You sent 1001 records. Send at most 1000 per batch.",
"details": { "limit": 1000, "received": 1001 }
}
}{
"error": {
"code": "payload_too_large",
"message": "A batch may be at most 4456448 bytes.",
"details": { "limit": 4456448 }
}
}HTTP/1.1 429 Too Many Requests
Retry-After: 60
{
"error": {
"code": "rate_limited",
"message": "Too many requests. Slow down and retry."
}
}