Contacts and labels
Read shared workspace contacts, upsert and update profiles, and apply existing labels with scoped API requests.
Reviewed
Contacts and labels belong to the whole workspace. They are not isolated to the key's sending number. Use the workspace contact scopes intentionally.
Write and read profiles
Upsert requires an international phoneE164. Reusing the same normalized phone updates the existing active contact. It does not restore a deleted contact. PATCH identifies the contact in the path and cannot change its phone number. An ordinary update requires at least one profile field; the explicit restoration contract below also permits a restore-only update.
Names are trimmed and limited to 80 characters. Email must be valid and at most 255 characters. Country code is at most four characters; locale is at most 16. These optional fields can be null. customAttributes merges scalar strings, finite numbers, booleans and null values. Keys have 1 to 64 characters, strings at most 500, and a request allows at most 1,000 entries. Existing field definitions and workspace attribute limits can reject a smaller request. Nested objects and arrays are not accepted.
Reads return at most 25 scalar custom attributes and truncate strings to 500 characters. This is a display projection, not a full export. Contact lists include optedOut; detail also includes up to 100 labelIds. Write responses contain the profile without these read-only fields. Contact projections also include nullable ISO archivedAt and nonnegative integer revision. This revision changes on archive/restore transitions only; it is not a general profile ETag. Public profile writes cannot clear opt-out or consent restrictions.
Restore an archived contact explicitly
Source contract, pending release. Published @waaru/sdk beta.4 rejects the new update fields and does not expose archived lookup. Use REST or the checked-in repository helper against the matching API release until an aligned SDK is released.
Read GET /v1/developer/contacts/:id?includeArchived=true to inspect an exact archived contact and its current lifecycle revision. Omission or false returns active contacts only. Lists remain active-only and do not accept this archive option. Read access still requires workspace:contacts:read.
PATCH /v1/developer/contacts/:id accepts optional restoreArchived (boolean) and expectedRevision (nonnegative integer). Setting restoreArchived: true requires expectedRevision, even when the contact is already active. You can restore without changing profile fields:
{
"restoreArchived": true,
"expectedRevision": 7
}Replace the illustrative revision with the value you just read. For an archived contact, a stale revision returns 409 contact_archive_changed; read and review it again before deciding to restore. Restoration and any supplied profile changes commit together in one transaction, with restoration and update audit records. If the contact is already active, restoration does nothing before the revision comparison; supplied profile changes still apply. Ordinary profile updates keep their existing concurrency behavior.
Omitting restoreArchived, or setting it false, preserves normal active-profile updates and returns 404 contact_not_found for an archived contact. A revision alone is not an update. Upsert never restores, and imports are unchanged. Restoration preserves opt-out, restrictions and history; former segment memberships stay removed. It does not authorize messaging. API-key writes still require workspace:contacts:write and a connected number in EXTERNAL_API mode.
Separately authorized OAuth clients can use GET /external/v1/contacts/find with exact phoneE164 and explicit includeArchived=true, then POST /external/v1/contacts/update with contactId and the same restoration fields. MCP find_contact and get_contact accept explicit includeArchived: true; update_contact accepts the same update fields. Defaults remain active-only. OAuth retains external:contacts:read/external:contacts:write; MCP retains its contact read/write scopes and current workspace authority. No new grant is created.
Discover Developer API capabilities in setup, the endpoint reference and downloadable OpenAPI. An authorized MCP client's scoped tools/list describes the tools currently available to that client.
Search and label
List accepts q (name, email or phone), inclusive updatedSince, limit and after. Resource pages sort by ID ascending; pass nextCursor as after with unchanged filters until null. These are live pages, not snapshot exports or a deletion feed.
List labels before assigning them. PUT and DELETE label assignments have no body and return HTTP 200 JSON. applied describes the desired final label state, so repeat requests are safe. Create and rename labels with workspace:labels:manage. Names use NFKC normalization, collapsed whitespace and case-insensitive uniqueness. Names contain 1 to 48 characters after normalization. Lifecycle request bodies are limited to 4 KiB. Archive preserves assignments and history but removes the label from selection and blocks new assignments. Existing assignments can still be removed. Archived names stay reserved. An active or pinned dynamic segment dependency returns 409 resource_in_use. Label hard deletion and restoration are not exposed. Creation has no idempotency key; reconcile a lost response before retrying.
Endpoint details
Every request uses Authorization: Bearer $WAARU_API_KEY and a connected number in Developer API mode. The scope below is required. IDs and payloads are illustrative; replace them with your own permitted resources. See authentication and common errors, size limits and retries.
POST /v1/developer/labels
Required scope: workspace:labels:manage
Requires workspace:labels:manage. Body limit: 4096 bytes. Requires an explicitly granted management scope. Archive preserves historical assignments and membership, reserves the name, and rejects active or pinned dynamic-segment dependencies. Repeat archive is safe; creation has no idempotency key.
No path or query parameters.
Request
curl --fail-with-body --request POST 'https://api.waaru.app/v1/developer/labels' \
--header "Authorization: Bearer $WAARU_API_KEY" \
--header 'Content-Type: application/json' \
--data-binary @request.jsonSave this illustrative JSON as request.json. The body is required.
{
"name": "Customers"
}| Field | Required | Type and constraints |
|---|---|---|
name | Yes | {"type":"string","minLength":1,"description":"After NFKC normalization, trimming and whitespace collapsing, the name must contain 1 to 48 characters. The 48-character maximum does not apply to the raw input. The request body limit is 4096 bytes."} |
Request body: complete JSON schema
Required fields appear in required. Omit optional fields unless needed. Null is accepted only where the schema includes it. Arrays describe their elements in items; variants appear in anyOf or oneOf.
{
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"description": "After NFKC normalization, trimming and whitespace collapsing, the name must contain 1 to 48 characters. The 48-character maximum does not apply to the raw input. The request body limit is 4096 bytes."
}
},
"required": [
"name"
],
"additionalProperties": false
}Response: HTTP 200
{
"label": {
"id": "label_example",
"name": "Customers",
"archivedAt": null
}
}| Field | Required | Type and constraints |
|---|---|---|
label | Yes | {"type":"object","required":["id","name","archivedAt"],"additionalProperties":false} Nested fields are in the complete schema below. |
Response body: complete JSON schema
Required fields appear in required. Omit optional fields unless needed. Null is accepted only where the schema includes it. Arrays describe their elements in items; variants appear in anyOf or oneOf.
{
"type": "object",
"properties": {
"label": {
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1,
"maxLength": 191
},
"name": {
"type": "string"
},
"archivedAt": {
"type": [
"string",
"null"
],
"format": "date-time"
}
},
"required": [
"id",
"name",
"archivedAt"
],
"additionalProperties": false
}
},
"required": [
"label"
],
"additionalProperties": false
}Failures use a stable code and safe message. They never expose internal exception messages, traces or provider diagnostics. Check the error reference before retrying. An inaccessible resource can return 404 even if its identifier exists elsewhere.
GET /v1/developer/labels
Required scope: workspace:contacts:read
Requires workspace:contacts:read.
| Parameter | Location | Required | Constraints |
|---|---|---|---|
limit | query | No | {"type":"integer","minimum":1,"maximum":100,"default":50} |
after | query | No | {"type":"string","maxLength":512} Use nextCursor from the previous response; keep filters unchanged. |
Request
curl --fail-with-body --request GET 'https://api.waaru.app/v1/developer/labels' \
--header "Authorization: Bearer $WAARU_API_KEY"Send no request body.
Response: HTTP 200
{
"items": [
{
"id": "label_example",
"name": "Attendee"
}
],
"nextCursor": null
}| Field | Required | Type and constraints |
|---|---|---|
items | Yes | {"type":"array","maxItems":100} Nested fields are in the complete schema below. |
nextCursor | Yes | {"type":["string","null"]} |
Response body: complete JSON schema
Required fields appear in required. Omit optional fields unless needed. Null is accepted only where the schema includes it. Arrays describe their elements in items; variants appear in anyOf or oneOf.
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1,
"maxLength": 191
},
"name": {
"type": "string"
}
},
"required": [
"id",
"name"
],
"additionalProperties": false
},
"maxItems": 100
},
"nextCursor": {
"type": [
"string",
"null"
]
}
},
"required": [
"items",
"nextCursor"
],
"additionalProperties": false
}Failures use a stable code and safe message. They never expose internal exception messages, traces or provider diagnostics. Check the error reference before retrying. An inaccessible resource can return 404 even if its identifier exists elsewhere.
PATCH /v1/developer/labels/{id}
Required scope: workspace:labels:manage
Requires workspace:labels:manage. Body limit: 4096 bytes. Requires an explicitly granted management scope. Archive preserves historical assignments and membership, reserves the name, and rejects active or pinned dynamic-segment dependencies. Repeat archive is safe; creation has no idempotency key.
| Parameter | Location | Required | Constraints |
|---|---|---|---|
id | path | Yes | {"type":"string","minLength":1,"maxLength":191} |
Request
curl --fail-with-body --request PATCH 'https://api.waaru.app/v1/developer/labels/id_example' \
--header "Authorization: Bearer $WAARU_API_KEY" \
--header 'Content-Type: application/json' \
--data-binary @request.jsonSave this illustrative JSON as request.json. The body is required.
{
"name": "Returning customers"
}| Field | Required | Type and constraints |
|---|---|---|
name | Yes | {"type":"string","minLength":1,"description":"After NFKC normalization, trimming and whitespace collapsing, the name must contain 1 to 48 characters. The 48-character maximum does not apply to the raw input. The request body limit is 4096 bytes."} |
Request body: complete JSON schema
Required fields appear in required. Omit optional fields unless needed. Null is accepted only where the schema includes it. Arrays describe their elements in items; variants appear in anyOf or oneOf.
{
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"description": "After NFKC normalization, trimming and whitespace collapsing, the name must contain 1 to 48 characters. The 48-character maximum does not apply to the raw input. The request body limit is 4096 bytes."
}
},
"required": [
"name"
],
"additionalProperties": false
}Response: HTTP 200
{
"label": {
"id": "label_example",
"name": "Returning customers",
"archivedAt": null
}
}| Field | Required | Type and constraints |
|---|---|---|
label | Yes | {"type":"object","required":["id","name","archivedAt"],"additionalProperties":false} Nested fields are in the complete schema below. |
Response body: complete JSON schema
Required fields appear in required. Omit optional fields unless needed. Null is accepted only where the schema includes it. Arrays describe their elements in items; variants appear in anyOf or oneOf.
{
"type": "object",
"properties": {
"label": {
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1,
"maxLength": 191
},
"name": {
"type": "string"
},
"archivedAt": {
"type": [
"string",
"null"
],
"format": "date-time"
}
},
"required": [
"id",
"name",
"archivedAt"
],
"additionalProperties": false
}
},
"required": [
"label"
],
"additionalProperties": false
}Failures use a stable code and safe message. They never expose internal exception messages, traces or provider diagnostics. Check the error reference before retrying. An inaccessible resource can return 404 even if its identifier exists elsewhere.
POST /v1/developer/labels/{id}/archive
Required scope: workspace:labels:manage
Requires workspace:labels:manage. Body limit: 4096 bytes. Requires an explicitly granted management scope. Archive preserves historical assignments and membership, reserves the name, and rejects active or pinned dynamic-segment dependencies. Repeat archive is safe; creation has no idempotency key.
| Parameter | Location | Required | Constraints |
|---|---|---|---|
id | path | Yes | {"type":"string","minLength":1,"maxLength":191} |
Request
curl --fail-with-body --request POST 'https://api.waaru.app/v1/developer/labels/id_example/archive' \
--header "Authorization: Bearer $WAARU_API_KEY" \
--header 'Content-Type: application/json' \
--data-binary @request.jsonSave this illustrative JSON as request.json. The body is required.
{}| Field | Required | Type and constraints |
|---|
Request body: complete JSON schema
Required fields appear in required. Omit optional fields unless needed. Null is accepted only where the schema includes it. Arrays describe their elements in items; variants appear in anyOf or oneOf.
{
"type": "object",
"properties": {},
"required": [],
"additionalProperties": false
}Response: HTTP 200
{
"label": {
"id": "label_example",
"name": "Customers",
"archivedAt": "2026-10-04T00:00:00.000Z"
}
}| Field | Required | Type and constraints |
|---|---|---|
label | Yes | {"type":"object","required":["id","name","archivedAt"],"additionalProperties":false} Nested fields are in the complete schema below. |
Response body: complete JSON schema
Required fields appear in required. Omit optional fields unless needed. Null is accepted only where the schema includes it. Arrays describe their elements in items; variants appear in anyOf or oneOf.
{
"type": "object",
"properties": {
"label": {
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1,
"maxLength": 191
},
"name": {
"type": "string"
},
"archivedAt": {
"type": [
"string",
"null"
],
"format": "date-time"
}
},
"required": [
"id",
"name",
"archivedAt"
],
"additionalProperties": false
}
},
"required": [
"label"
],
"additionalProperties": false
}Failures use a stable code and safe message. They never expose internal exception messages, traces or provider diagnostics. Check the error reference before retrying. An inaccessible resource can return 404 even if its identifier exists elsewhere.
GET /v1/developer/contacts
Required scope: workspace:contacts:read
Requires workspace:contacts:read.
| Parameter | Location | Required | Constraints |
|---|---|---|---|
limit | query | No | {"type":"integer","minimum":1,"maximum":100,"default":50} |
after | query | No | {"type":"string","maxLength":512} Use nextCursor from the previous response; keep filters unchanged. |
q | query | No | {"type":"string","minLength":1,"maxLength":256} |
updatedSince | query | No | {"type":"string","format":"date-time"} |
Request
curl --fail-with-body --request GET 'https://api.waaru.app/v1/developer/contacts' \
--header "Authorization: Bearer $WAARU_API_KEY"Send no request body.
Response: HTTP 200
{
"items": [
{
"id": "contact_example",
"phoneE164": "+919876543210",
"firstName": "Asha",
"lastName": null,
"email": null,
"countryCode": "IN",
"locale": "en",
"customAttributes": {
"customerTier": "regular"
},
"createdAt": "2026-10-01T09:00:00.000Z",
"updatedAt": "2026-10-01T09:00:00.000Z",
"optedOut": false,
"archivedAt": null,
"revision": 1
}
],
"nextCursor": null
}| Field | Required | Type and constraints |
|---|---|---|
items | Yes | {"type":"array","maxItems":100} Nested fields are in the complete schema below. |
nextCursor | Yes | {"type":["string","null"]} |
Response body: complete JSON schema
Required fields appear in required. Omit optional fields unless needed. Null is accepted only where the schema includes it. Arrays describe their elements in items; variants appear in anyOf or oneOf.
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1,
"maxLength": 191
},
"phoneE164": {
"type": "string"
},
"firstName": {
"type": [
"string",
"null"
]
},
"lastName": {
"type": [
"string",
"null"
]
},
"email": {
"anyOf": [
{
"type": "string",
"maxLength": 255,
"format": "email",
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
},
{
"type": "null"
}
]
},
"countryCode": {
"anyOf": [
{
"type": "string",
"maxLength": 4
},
{
"type": "null"
}
]
},
"locale": {
"anyOf": [
{
"type": "string",
"maxLength": 16
},
{
"type": "null"
}
]
},
"customAttributes": {
"type": "object",
"propertyNames": {
"type": "string",
"minLength": 1,
"maxLength": 64
},
"additionalProperties": {
"anyOf": [
{
"type": "string",
"maxLength": 500
},
{
"type": "number"
},
{
"type": "boolean"
},
{
"type": "null"
}
]
}
},
"createdAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
},
"updatedAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
},
"archivedAt": {
"anyOf": [
{
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
},
{
"type": "null"
}
]
},
"revision": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"optedOut": {
"type": "boolean"
},
"labelIds": {
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 191
},
"maxItems": 100
}
},
"required": [
"id",
"phoneE164",
"firstName",
"lastName",
"email",
"countryCode",
"locale",
"customAttributes",
"createdAt",
"updatedAt",
"archivedAt",
"revision",
"optedOut"
],
"additionalProperties": false
},
"maxItems": 100
},
"nextCursor": {
"type": [
"string",
"null"
]
}
},
"required": [
"items",
"nextCursor"
],
"additionalProperties": false
}Failures use a stable code and safe message. They never expose internal exception messages, traces or provider diagnostics. Check the error reference before retrying. An inaccessible resource can return 404 even if its identifier exists elsewhere.
POST /v1/developer/contacts
Required scope: workspace:contacts:write
Requires workspace:contacts:write.
No path or query parameters.
Request
curl --fail-with-body --request POST 'https://api.waaru.app/v1/developer/contacts' \
--header "Authorization: Bearer $WAARU_API_KEY" \
--header 'Content-Type: application/json' \
--data-binary @request.jsonSave this illustrative JSON as request.json. The body is required.
{
"phoneE164": "+919876543210",
"firstName": "Asha",
"countryCode": "IN",
"locale": "en",
"customAttributes": {
"customerTier": "regular"
}
}| Field | Required | Type and constraints |
|---|---|---|
firstName | No | {"anyOf":[{"type":"string","maxLength":80},{"type":"null"}]} |
lastName | No | {"anyOf":[{"type":"string","maxLength":80},{"type":"null"}]} |
email | No | {"anyOf":[{"type":"string","maxLength":255,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]} |
countryCode | No | {"anyOf":[{"type":"string","maxLength":4},{"type":"null"}]} |
locale | No | {"anyOf":[{"type":"string","maxLength":16},{"type":"null"}]} |
customAttributes | No | {"type":"object","propertyNames":{"type":"string","minLength":1,"maxLength":64},"additionalProperties":{"anyOf":[{"type":"string","maxLength":500},{"type":"number"},{"type":"boolean"},{"type":"null"}]}} |
phoneE164 | Yes | {"type":"string"} |
Request body: complete JSON schema
Required fields appear in required. Omit optional fields unless needed. Null is accepted only where the schema includes it. Arrays describe their elements in items; variants appear in anyOf or oneOf.
{
"type": "object",
"properties": {
"firstName": {
"anyOf": [
{
"type": "string",
"maxLength": 80
},
{
"type": "null"
}
]
},
"lastName": {
"anyOf": [
{
"type": "string",
"maxLength": 80
},
{
"type": "null"
}
]
},
"email": {
"anyOf": [
{
"type": "string",
"maxLength": 255,
"format": "email",
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
},
{
"type": "null"
}
]
},
"countryCode": {
"anyOf": [
{
"type": "string",
"maxLength": 4
},
{
"type": "null"
}
]
},
"locale": {
"anyOf": [
{
"type": "string",
"maxLength": 16
},
{
"type": "null"
}
]
},
"customAttributes": {
"type": "object",
"propertyNames": {
"type": "string",
"minLength": 1,
"maxLength": 64
},
"additionalProperties": {
"anyOf": [
{
"type": "string",
"maxLength": 500
},
{
"type": "number"
},
{
"type": "boolean"
},
{
"type": "null"
}
]
}
},
"phoneE164": {
"type": "string"
}
},
"required": [
"phoneE164"
],
"additionalProperties": false
}Response: HTTP 200
{
"contact": {
"id": "contact_example",
"phoneE164": "+919876543210",
"firstName": "Asha",
"lastName": null,
"email": null,
"countryCode": "IN",
"locale": "en",
"customAttributes": {
"customerTier": "regular"
},
"createdAt": "2026-10-01T09:00:00.000Z",
"updatedAt": "2026-10-01T09:00:00.000Z",
"archivedAt": null,
"revision": 1
}
}| Field | Required | Type and constraints |
|---|---|---|
contact | Yes | {"type":"object","required":["id","phoneE164","firstName","lastName","email","countryCode","locale","customAttributes","createdAt","updatedAt","archivedAt","revision"],"additionalProperties":false} Nested fields are in the complete schema below. |
Response body: complete JSON schema
Required fields appear in required. Omit optional fields unless needed. Null is accepted only where the schema includes it. Arrays describe their elements in items; variants appear in anyOf or oneOf.
{
"type": "object",
"properties": {
"contact": {
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1,
"maxLength": 191
},
"phoneE164": {
"type": "string"
},
"firstName": {
"type": [
"string",
"null"
]
},
"lastName": {
"type": [
"string",
"null"
]
},
"email": {
"anyOf": [
{
"type": "string",
"maxLength": 255,
"format": "email",
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
},
{
"type": "null"
}
]
},
"countryCode": {
"anyOf": [
{
"type": "string",
"maxLength": 4
},
{
"type": "null"
}
]
},
"locale": {
"anyOf": [
{
"type": "string",
"maxLength": 16
},
{
"type": "null"
}
]
},
"customAttributes": {
"type": "object",
"propertyNames": {
"type": "string",
"minLength": 1,
"maxLength": 64
},
"additionalProperties": {
"anyOf": [
{
"type": "string",
"maxLength": 500
},
{
"type": "number"
},
{
"type": "boolean"
},
{
"type": "null"
}
]
}
},
"createdAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
},
"updatedAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
},
"archivedAt": {
"anyOf": [
{
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
},
{
"type": "null"
}
]
},
"revision": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}
},
"required": [
"id",
"phoneE164",
"firstName",
"lastName",
"email",
"countryCode",
"locale",
"customAttributes",
"createdAt",
"updatedAt",
"archivedAt",
"revision"
],
"additionalProperties": false
}
},
"required": [
"contact"
],
"additionalProperties": false
}Failures use a stable code and safe message. They never expose internal exception messages, traces or provider diagnostics. Check the error reference before retrying. An inaccessible resource can return 404 even if its identifier exists elsewhere.
GET /v1/developer/contacts/{id}
Required scope: workspace:contacts:read
Requires workspace:contacts:read. Active contacts by default. Explicit includeArchived=true on this exact-ID read exposes archivedAt and lifecycle revision for reviewed restoration; lists remain active-only.
| Parameter | Location | Required | Constraints |
|---|---|---|---|
id | path | Yes | {"type":"string","minLength":1,"maxLength":191} |
includeArchived | query | No | {"type":"boolean","default":false} Explicit exact-contact archive lookup; returns archivedAt and revision for restoration. |
Request
curl --fail-with-body --request GET 'https://api.waaru.app/v1/developer/contacts/id_example' \
--header "Authorization: Bearer $WAARU_API_KEY"Send no request body.
Response: HTTP 200
{
"contact": {
"id": "contact_example",
"phoneE164": "+919876543210",
"firstName": "Asha",
"lastName": null,
"email": null,
"countryCode": "IN",
"locale": "en",
"customAttributes": {
"customerTier": "regular"
},
"createdAt": "2026-10-01T09:00:00.000Z",
"updatedAt": "2026-10-01T09:00:00.000Z",
"optedOut": false,
"labelIds": [
"label_example"
],
"archivedAt": null,
"revision": 1
}
}| Field | Required | Type and constraints |
|---|---|---|
contact | Yes | {"type":"object","required":["id","phoneE164","firstName","lastName","email","countryCode","locale","customAttributes","createdAt","updatedAt","archivedAt","revision","optedOut"],"additionalProperties":false} Nested fields are in the complete schema below. |
Response body: complete JSON schema
Required fields appear in required. Omit optional fields unless needed. Null is accepted only where the schema includes it. Arrays describe their elements in items; variants appear in anyOf or oneOf.
{
"type": "object",
"properties": {
"contact": {
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1,
"maxLength": 191
},
"phoneE164": {
"type": "string"
},
"firstName": {
"type": [
"string",
"null"
]
},
"lastName": {
"type": [
"string",
"null"
]
},
"email": {
"anyOf": [
{
"type": "string",
"maxLength": 255,
"format": "email",
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
},
{
"type": "null"
}
]
},
"countryCode": {
"anyOf": [
{
"type": "string",
"maxLength": 4
},
{
"type": "null"
}
]
},
"locale": {
"anyOf": [
{
"type": "string",
"maxLength": 16
},
{
"type": "null"
}
]
},
"customAttributes": {
"type": "object",
"propertyNames": {
"type": "string",
"minLength": 1,
"maxLength": 64
},
"additionalProperties": {
"anyOf": [
{
"type": "string",
"maxLength": 500
},
{
"type": "number"
},
{
"type": "boolean"
},
{
"type": "null"
}
]
}
},
"createdAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
},
"updatedAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
},
"archivedAt": {
"anyOf": [
{
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
},
{
"type": "null"
}
]
},
"revision": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"optedOut": {
"type": "boolean"
},
"labelIds": {
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 191
},
"maxItems": 100
}
},
"required": [
"id",
"phoneE164",
"firstName",
"lastName",
"email",
"countryCode",
"locale",
"customAttributes",
"createdAt",
"updatedAt",
"archivedAt",
"revision",
"optedOut"
],
"additionalProperties": false
}
},
"required": [
"contact"
],
"additionalProperties": false
}Failures use a stable code and safe message. They never expose internal exception messages, traces or provider diagnostics. Check the error reference before retrying. An inaccessible resource can return 404 even if its identifier exists elsewhere.
PATCH /v1/developer/contacts/{id}
Required scope: workspace:contacts:write
Requires workspace:contacts:write. Update profile fields or explicitly restoreArchived:true with expectedRevision from an exact archived read. Omitted/false never restores; stale archived revision returns 409 contact_archive_changed. Restore and profile changes commit together without changing opt-outs, restrictions or history, or recreating removed segment memberships. Active restoration is a no-op before revision comparison; ordinary profile concurrency is unchanged.
| Parameter | Location | Required | Constraints |
|---|---|---|---|
id | path | Yes | {"type":"string","minLength":1,"maxLength":191} |
Request
curl --fail-with-body --request PATCH 'https://api.waaru.app/v1/developer/contacts/id_example' \
--header "Authorization: Bearer $WAARU_API_KEY" \
--header 'Content-Type: application/json' \
--data-binary @request.jsonSave this illustrative JSON as request.json. The body is required.
{
"firstName": "Asha"
}| Field | Required | Type and constraints |
|---|---|---|
firstName | No | {"anyOf":[{"type":"string","maxLength":80},{"type":"null"}]} |
lastName | No | {"anyOf":[{"type":"string","maxLength":80},{"type":"null"}]} |
email | No | {"anyOf":[{"type":"string","maxLength":255,"format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}]} |
countryCode | No | {"anyOf":[{"type":"string","maxLength":4},{"type":"null"}]} |
locale | No | {"anyOf":[{"type":"string","maxLength":16},{"type":"null"}]} |
customAttributes | No | {"type":"object","propertyNames":{"type":"string","minLength":1,"maxLength":64},"additionalProperties":{"anyOf":[{"type":"string","maxLength":500},{"type":"number"},{"type":"boolean"},{"type":"null"}]}} |
restoreArchived | No | {"type":"boolean"} |
expectedRevision | No | {"type":"integer","minimum":0,"maximum":9007199254740991} |
Request body: complete JSON schema
Required fields appear in required. Omit optional fields unless needed. Null is accepted only where the schema includes it. Arrays describe their elements in items; variants appear in anyOf or oneOf.
{
"type": "object",
"properties": {
"firstName": {
"anyOf": [
{
"type": "string",
"maxLength": 80
},
{
"type": "null"
}
]
},
"lastName": {
"anyOf": [
{
"type": "string",
"maxLength": 80
},
{
"type": "null"
}
]
},
"email": {
"anyOf": [
{
"type": "string",
"maxLength": 255,
"format": "email",
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
},
{
"type": "null"
}
]
},
"countryCode": {
"anyOf": [
{
"type": "string",
"maxLength": 4
},
{
"type": "null"
}
]
},
"locale": {
"anyOf": [
{
"type": "string",
"maxLength": 16
},
{
"type": "null"
}
]
},
"customAttributes": {
"type": "object",
"propertyNames": {
"type": "string",
"minLength": 1,
"maxLength": 64
},
"additionalProperties": {
"anyOf": [
{
"type": "string",
"maxLength": 500
},
{
"type": "number"
},
{
"type": "boolean"
},
{
"type": "null"
}
]
}
},
"restoreArchived": {
"type": "boolean"
},
"expectedRevision": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}
},
"additionalProperties": false,
"minProperties": 1,
"allOf": [
{
"anyOf": [
{
"required": [
"firstName"
]
},
{
"required": [
"lastName"
]
},
{
"required": [
"email"
]
},
{
"required": [
"countryCode"
]
},
{
"required": [
"locale"
]
},
{
"required": [
"customAttributes"
]
},
{
"required": [
"restoreArchived"
],
"properties": {
"restoreArchived": {
"const": true
}
}
}
]
},
{
"if": {
"required": [
"restoreArchived"
],
"properties": {
"restoreArchived": {
"const": true
}
}
},
"then": {
"required": [
"expectedRevision"
]
}
}
]
}Response: HTTP 200
{
"contact": {
"id": "contact_example",
"phoneE164": "+919876543210",
"firstName": "Asha",
"lastName": null,
"email": null,
"countryCode": "IN",
"locale": "en",
"customAttributes": {
"customerTier": "regular"
},
"createdAt": "2026-10-01T09:00:00.000Z",
"updatedAt": "2026-10-01T09:00:00.000Z",
"archivedAt": null,
"revision": 1
}
}| Field | Required | Type and constraints |
|---|---|---|
contact | Yes | {"type":"object","required":["id","phoneE164","firstName","lastName","email","countryCode","locale","customAttributes","createdAt","updatedAt","archivedAt","revision"],"additionalProperties":false} Nested fields are in the complete schema below. |
Response body: complete JSON schema
Required fields appear in required. Omit optional fields unless needed. Null is accepted only where the schema includes it. Arrays describe their elements in items; variants appear in anyOf or oneOf.
{
"type": "object",
"properties": {
"contact": {
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1,
"maxLength": 191
},
"phoneE164": {
"type": "string"
},
"firstName": {
"type": [
"string",
"null"
]
},
"lastName": {
"type": [
"string",
"null"
]
},
"email": {
"anyOf": [
{
"type": "string",
"maxLength": 255,
"format": "email",
"pattern": "^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
},
{
"type": "null"
}
]
},
"countryCode": {
"anyOf": [
{
"type": "string",
"maxLength": 4
},
{
"type": "null"
}
]
},
"locale": {
"anyOf": [
{
"type": "string",
"maxLength": 16
},
{
"type": "null"
}
]
},
"customAttributes": {
"type": "object",
"propertyNames": {
"type": "string",
"minLength": 1,
"maxLength": 64
},
"additionalProperties": {
"anyOf": [
{
"type": "string",
"maxLength": 500
},
{
"type": "number"
},
{
"type": "boolean"
},
{
"type": "null"
}
]
}
},
"createdAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
},
"updatedAt": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
},
"archivedAt": {
"anyOf": [
{
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
},
{
"type": "null"
}
]
},
"revision": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}
},
"required": [
"id",
"phoneE164",
"firstName",
"lastName",
"email",
"countryCode",
"locale",
"customAttributes",
"createdAt",
"updatedAt",
"archivedAt",
"revision"
],
"additionalProperties": false
}
},
"required": [
"contact"
],
"additionalProperties": false
}Failures use a stable code and safe message. They never expose internal exception messages, traces or provider diagnostics. Check the error reference before retrying. An inaccessible resource can return 404 even if its identifier exists elsewhere.
PUT /v1/developer/contacts/{id}/labels/{labelId}
Required scope: workspace:contacts:write
Requires workspace:contacts:write.
| Parameter | Location | Required | Constraints |
|---|---|---|---|
id | path | Yes | {"type":"string","minLength":1,"maxLength":191} |
labelId | path | Yes | {"type":"string","minLength":1,"maxLength":191} |
Request
curl --fail-with-body --request PUT 'https://api.waaru.app/v1/developer/contacts/id_example/labels/labelId_example' \
--header "Authorization: Bearer $WAARU_API_KEY"Send no request body.
Response: HTTP 200
{
"contactId": "contact_example",
"labelId": "label_example",
"applied": true
}| Field | Required | Type and constraints |
|---|---|---|
contactId | Yes | {"type":"string","minLength":1,"maxLength":191} |
labelId | Yes | {"type":"string","minLength":1,"maxLength":191} |
applied | Yes | {"type":"boolean"} |
Response body: complete JSON schema
Required fields appear in required. Omit optional fields unless needed. Null is accepted only where the schema includes it. Arrays describe their elements in items; variants appear in anyOf or oneOf.
{
"type": "object",
"properties": {
"contactId": {
"type": "string",
"minLength": 1,
"maxLength": 191
},
"labelId": {
"type": "string",
"minLength": 1,
"maxLength": 191
},
"applied": {
"type": "boolean"
}
},
"required": [
"contactId",
"labelId",
"applied"
],
"additionalProperties": false
}Failures use a stable code and safe message. They never expose internal exception messages, traces or provider diagnostics. Check the error reference before retrying. An inaccessible resource can return 404 even if its identifier exists elsewhere.
DELETE /v1/developer/contacts/{id}/labels/{labelId}
Required scope: workspace:contacts:write
Requires workspace:contacts:write.
| Parameter | Location | Required | Constraints |
|---|---|---|---|
id | path | Yes | {"type":"string","minLength":1,"maxLength":191} |
labelId | path | Yes | {"type":"string","minLength":1,"maxLength":191} |
Request
curl --fail-with-body --request DELETE 'https://api.waaru.app/v1/developer/contacts/id_example/labels/labelId_example' \
--header "Authorization: Bearer $WAARU_API_KEY"Send no request body.
Response: HTTP 200
{
"contactId": "contact_example",
"labelId": "label_example",
"applied": false
}| Field | Required | Type and constraints |
|---|---|---|
contactId | Yes | {"type":"string","minLength":1,"maxLength":191} |
labelId | Yes | {"type":"string","minLength":1,"maxLength":191} |
applied | Yes | {"type":"boolean"} |
Response body: complete JSON schema
Required fields appear in required. Omit optional fields unless needed. Null is accepted only where the schema includes it. Arrays describe their elements in items; variants appear in anyOf or oneOf.
{
"type": "object",
"properties": {
"contactId": {
"type": "string",
"minLength": 1,
"maxLength": 191
},
"labelId": {
"type": "string",
"minLength": 1,
"maxLength": 191
},
"applied": {
"type": "boolean"
}
},
"required": [
"contactId",
"labelId",
"applied"
],
"additionalProperties": false
}Failures use a stable code and safe message. They never expose internal exception messages, traces or provider diagnostics. Check the error reference before retrying. An inaccessible resource can return 404 even if its identifier exists elsewhere.
Next: All public API operations or authentication and scopes.