Skip to content
WaaruDocs

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.json

Save this illustrative JSON as request.json. The body is required.

{
  "name": "Customers"
}
Request body fields
FieldRequiredType and constraints
nameYes{"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
  }
}
Response body fields
FieldRequiredType and constraints
labelYes{"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.

Request parameters
ParameterLocationRequiredConstraints
limitqueryNo{"type":"integer","minimum":1,"maximum":100,"default":50}
afterqueryNo{"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
}
Response body fields
FieldRequiredType and constraints
itemsYes{"type":"array","maxItems":100} Nested fields are in the complete schema below.
nextCursorYes{"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.

Request parameters
ParameterLocationRequiredConstraints
idpathYes{"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.json

Save this illustrative JSON as request.json. The body is required.

{
  "name": "Returning customers"
}
Request body fields
FieldRequiredType and constraints
nameYes{"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
  }
}
Response body fields
FieldRequiredType and constraints
labelYes{"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.

Request parameters
ParameterLocationRequiredConstraints
idpathYes{"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.json

Save this illustrative JSON as request.json. The body is required.

{}
Request body fields
FieldRequiredType 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"
  }
}
Response body fields
FieldRequiredType and constraints
labelYes{"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.

Request parameters
ParameterLocationRequiredConstraints
limitqueryNo{"type":"integer","minimum":1,"maximum":100,"default":50}
afterqueryNo{"type":"string","maxLength":512} Use nextCursor from the previous response; keep filters unchanged.
qqueryNo{"type":"string","minLength":1,"maxLength":256}
updatedSincequeryNo{"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
}
Response body fields
FieldRequiredType and constraints
itemsYes{"type":"array","maxItems":100} Nested fields are in the complete schema below.
nextCursorYes{"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.json

Save this illustrative JSON as request.json. The body is required.

{
  "phoneE164": "+919876543210",
  "firstName": "Asha",
  "countryCode": "IN",
  "locale": "en",
  "customAttributes": {
    "customerTier": "regular"
  }
}
Request body fields
FieldRequiredType and constraints
firstNameNo{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}]}
lastNameNo{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}]}
emailNo{"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"}]}
countryCodeNo{"anyOf":[{"type":"string","maxLength":4},{"type":"null"}]}
localeNo{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}]}
customAttributesNo{"type":"object","propertyNames":{"type":"string","minLength":1,"maxLength":64},"additionalProperties":{"anyOf":[{"type":"string","maxLength":500},{"type":"number"},{"type":"boolean"},{"type":"null"}]}}
phoneE164Yes{"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
  }
}
Response body fields
FieldRequiredType and constraints
contactYes{"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.

Request parameters
ParameterLocationRequiredConstraints
idpathYes{"type":"string","minLength":1,"maxLength":191}
includeArchivedqueryNo{"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
  }
}
Response body fields
FieldRequiredType and constraints
contactYes{"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.

Request parameters
ParameterLocationRequiredConstraints
idpathYes{"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.json

Save this illustrative JSON as request.json. The body is required.

{
  "firstName": "Asha"
}
Request body fields
FieldRequiredType and constraints
firstNameNo{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}]}
lastNameNo{"anyOf":[{"type":"string","maxLength":80},{"type":"null"}]}
emailNo{"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"}]}
countryCodeNo{"anyOf":[{"type":"string","maxLength":4},{"type":"null"}]}
localeNo{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}]}
customAttributesNo{"type":"object","propertyNames":{"type":"string","minLength":1,"maxLength":64},"additionalProperties":{"anyOf":[{"type":"string","maxLength":500},{"type":"number"},{"type":"boolean"},{"type":"null"}]}}
restoreArchivedNo{"type":"boolean"}
expectedRevisionNo{"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
  }
}
Response body fields
FieldRequiredType and constraints
contactYes{"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.

Request parameters
ParameterLocationRequiredConstraints
idpathYes{"type":"string","minLength":1,"maxLength":191}
labelIdpathYes{"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
}
Response body fields
FieldRequiredType and constraints
contactIdYes{"type":"string","minLength":1,"maxLength":191}
labelIdYes{"type":"string","minLength":1,"maxLength":191}
appliedYes{"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.

Request parameters
ParameterLocationRequiredConstraints
idpathYes{"type":"string","minLength":1,"maxLength":191}
labelIdpathYes{"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
}
Response body fields
FieldRequiredType and constraints
contactIdYes{"type":"string","minLength":1,"maxLength":191}
labelIdYes{"type":"string","minLength":1,"maxLength":191}
appliedYes{"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.

On this page