openapi: 3.2.0 info: title: Bird Email Contacts API version: 1.0.0 description: 'The Bird API: one REST API for email, SMS, WhatsApp, verification, and Realtime.' servers: - url: https://{region}.platform.bird.com description: 'Regional API endpoint. Use the host for the region your organization is hosted in. Official Bird SDKs and the CLI select it automatically from your API key, so you rarely need to set it by hand. ' variables: region: default: us1 enum: - us1 - eu1 description: The region your organization's data is hosted in. - url: https://platform.bird.com description: Region-independent endpoint for authentication and account administration. - url: http://localhost:8080 description: Local development. security: - BearerAuth: [] tags: - name: email-contacts description: Contacts are the people you send broadcasts to. Each contact is unique by email address within a workspace and carries optional name fields and custom properties for personalization. Custom properties are defined once per workspace via the contact properties API and then set per contact. paths: /v1/contacts: post: operationId: createContact x-snippet-key: contacts.create summary: Create a contact description: 'Creates a contact in the workspace, identified by an email address, a phone number, or both; at least one is required. Email is stored trimmed and lowercased, and phone in its canonical international form. Creating a second contact with the same email or phone number, or reusing another contact''s `external_id`, returns a conflict error. To create or update many contacts in one request, or to write a contact without knowing whether the address already exists, use Create or update contacts in bulk instead.' tags: - email-contacts security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - command parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ContactCreateRequest' responses: '201': description: Contact created. headers: Idempotency-Replay: $ref: '#/components/headers/IdempotencyReplay' content: application/json: schema: $ref: '#/components/schemas/Contact' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - n8n - sdk get: operationId: listContacts x-snippet-key: contacts.list summary: List contacts description: Returns a paginated list of contacts in the workspace, newest first. Look up a single contact by its exact `email`, `phone_number`, or `external_id`, or search by email, first name, last name, or phone substring with `q`. Repeat `phone_number` to resolve up to 50 numbers to their contacts in one request, raising `limit` to at least the number of values you pass. Pass `include_total=true` to add the total number of matching contacts to the response. tags: - email-contacts security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - command parameters: - name: email in: query required: false description: Return the contact with exactly this email address (case-insensitive). Email is unique within a workspace, so this matches at most one contact. An empty value is a validation error, never an unfiltered page. schema: type: string example: user@example.com - name: phone_number in: query required: false description: Return the contacts with exactly this phone number in international E.164 form. Repeat the parameter to match any of up to 50 numbers. Set `limit` to at least the number of values you pass. The default `limit` is 25, and a page cut short by it looks exactly like numbers that matched nothing. Different identifier parameters still combine with AND, so `phone_number=a&phone_number=b&email=c` asks for a contact whose phone number is `a` or `b` and whose email is `c`. Encode the leading plus sign as `%2B` (an unencoded `+` arrives as a space and is rejected). Phone numbers are unique within a workspace, so each value matches at most one contact. Non-canonical forms of the same number match the contact they canonicalize to; a value that is not a phone number shape, or an empty value, is a validation error, never an unfiltered page. schema: type: array maxItems: 50 items: type: string example: - '+31612345678' - '+31698765432' - name: external_id in: query required: false description: Return the contact with exactly this external_id (your own identifier for the contact). Unique within a workspace, so this matches at most one contact. An empty value is a validation error, never an unfiltered page. schema: type: string example: user_12345 - name: q in: query required: false description: Case-insensitive substring match against the contact's email address, first name, last name, or phone number. Phone matching is over the digits of the international form, so a full pasted number, a formatted number, or trailing digits all match; a national form with a leading trunk zero does not. schema: type: string minLength: 1 example: acme.com - name: identifier in: query required: false description: Filter to contacts that have a specific identifier on file. schema: $ref: '#/components/schemas/ContactIdentifierFilter' example: email - $ref: '#/components/parameters/PaginationLimit' - $ref: '#/components/parameters/StartingAfter' - $ref: '#/components/parameters/EndingBefore' - $ref: '#/components/parameters/IncludeTotal' responses: '200': description: A page of contacts. content: application/json: schema: $ref: '#/components/schemas/ContactList' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' x-surfaces: - attio - cli - make - mcp - n8n - sdk /v1/contacts/batch: post: operationId: createContactBatch x-snippet-key: contacts.batch summary: Create or update contacts in bulk description: 'Creates or updates up to 1,000 contacts in one request. Each entry is matched automatically against every identifier it supplies: its email address (trimmed and lowercased), its phone number (normalized to international form), and your own `external_id`. An entry with no match creates a contact. An entry whose identifiers all match one contact updates the supplied fields and preserves omitted fields. This lets an email address change under a stable `external_id` without creating a second contact. An entry whose identifiers belong to several contacts fails with an error naming each match; contacts are never merged automatically. Supplying `match_on` makes that field the only matching key, and every entry must include it. You can also add every contact in the request to up to 10 audiences. Each entry succeeds or fails on its own: the response lists one result per contact in submission order (`created`, `updated`, or `failed` with the reason), and a failed entry does not abort the rest. If the request itself is invalid, for example when an entry in `audience_ids` does not exist, the whole request fails with a validation error and no contacts are written.' tags: - email-contacts security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - command parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ContactUpsertRequest' responses: '200': description: Per-contact results, in submission order. headers: Idempotency-Replay: $ref: '#/components/headers/IdempotencyReplay' content: application/json: schema: $ref: '#/components/schemas/ContactUpsertResult' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - attio - cli - make - mcp - n8n - sdk /v1/contacts/{contact_id}: get: operationId: getContact x-snippet-key: contacts.get summary: Get a contact description: Returns a single contact, including its custom `data` values and the channels it can be reached on. To find a contact's ID by email address or `external_id`, use List contacts. tags: - email-contacts security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - command parameters: - name: contact_id in: path required: true description: ID of the contact to fetch. schema: $ref: '#/components/schemas/ContactID' responses: '200': description: The contact. content: application/json: schema: $ref: '#/components/schemas/Contact' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' x-surfaces: - attio - cli - make - mcp - n8n - sdk patch: operationId: updateContact x-snippet-key: contacts.update summary: Update a contact description: 'Updates a contact. Supplied fields are changed and omitted fields are left unchanged; set `first_name`, `last_name`, or `external_id` to `null` to clear them. Custom values in `data` are merged: keys you supply are set, keys set to `null` are removed, and keys you omit are unchanged. Changing the email address, phone number, or `external_id` to a value already used by another contact returns a conflict error. A contact always keeps at least one identifier. Clearing both email and phone in the same contact is rejected.' tags: - email-contacts security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - command parameters: - name: contact_id in: path required: true description: ID of the contact to update. schema: $ref: '#/components/schemas/ContactID' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ContactUpdateRequest' responses: '200': description: The updated contact. content: application/json: schema: $ref: '#/components/schemas/Contact' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - n8n - sdk delete: operationId: deleteContact x-snippet-key: contacts.delete summary: Delete a contact description: 'Deletes a contact permanently and removes it from every audience it belongs to. Suppression records for the address are not affected: an unsubscribed or bounced address stays suppressed even after the contact is deleted.' tags: - email-contacts security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - command parameters: - name: contact_id in: path required: true description: ID of the contact to delete. schema: $ref: '#/components/schemas/ContactID' - $ref: '#/components/parameters/IdempotencyKey' responses: '204': description: Contact deleted. '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - n8n - sdk /v1/contact-properties: post: operationId: createContactProperty x-snippet-key: contact_properties.create summary: Create a contact property description: 'Defines a custom property that contacts in the workspace can carry. The key becomes available in contact `data` and as a template variable in broadcasts. The key and type cannot be changed after creation. A key already in use returns a conflict error. A workspace can hold at most 200 properties; archived properties keep their key and count toward that limit.' tags: - email-contacts security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - command parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ContactPropertyCreateRequest' responses: '201': description: Contact property created. headers: Idempotency-Replay: $ref: '#/components/headers/IdempotencyReplay' content: application/json: schema: $ref: '#/components/schemas/ContactProperty' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - n8n - sdk get: operationId: listContactProperties x-snippet-key: contact_properties.list summary: List contact properties description: Returns a paginated list of the workspace's contact properties, newest first. Archived properties are included; check each entry's `archived` flag. tags: - email-contacts security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - command parameters: - $ref: '#/components/parameters/PaginationLimit' - $ref: '#/components/parameters/StartingAfter' - $ref: '#/components/parameters/EndingBefore' responses: '200': description: A page of contact properties. content: application/json: schema: $ref: '#/components/schemas/ContactPropertyList' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' x-surfaces: - cli - make - mcp - n8n - sdk /v1/contact-properties/{property_id}: get: operationId: getContactProperty x-snippet-key: contact_properties.get summary: Get a contact property description: 'Returns a single contact property: its immutable key and type, the fallback value, and whether it is archived.' tags: - email-contacts security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - command parameters: - name: property_id in: path required: true description: ID of the contact property to fetch. schema: $ref: '#/components/schemas/ContactPropertyID' responses: '200': description: The contact property. content: application/json: schema: $ref: '#/components/schemas/ContactProperty' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' x-surfaces: - cli - make - mcp - n8n - sdk patch: operationId: updateContactProperty x-snippet-key: contact_properties.update summary: Update a contact property description: Updates a contact property's fallback value, the only mutable field. The key and type cannot be changed after creation; create a new property instead. tags: - email-contacts security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - command parameters: - name: property_id in: path required: true description: ID of the contact property to update. schema: $ref: '#/components/schemas/ContactPropertyID' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ContactPropertyUpdateRequest' responses: '200': description: The updated contact property. content: application/json: schema: $ref: '#/components/schemas/ContactProperty' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - n8n - sdk /v1/contact-properties/{property_id}/archive: post: operationId: archiveContactProperty x-snippet-key: contact_properties.archive summary: Archive a contact property description: 'Archives a contact property. The key stops being accepted in contact writes and stops rendering in templates, but every value already stored on your contacts is preserved and still returned when you read a contact. The key stays reserved and still counts toward the workspace''s 200-property limit, so it cannot be re-created with a different type. Archiving an already-archived property returns a conflict error; reverse it with Unarchive a contact property.' tags: - email-contacts security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - command parameters: - name: property_id in: path required: true description: ID of the contact property to archive. schema: $ref: '#/components/schemas/ContactPropertyID' - $ref: '#/components/parameters/IdempotencyKey' responses: '200': description: The archived contact property. content: application/json: schema: $ref: '#/components/schemas/ContactProperty' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - n8n - sdk /v1/contact-properties/{property_id}/unarchive: post: operationId: unarchiveContactProperty x-snippet-key: contact_properties.unarchive summary: Unarchive a contact property description: Reactivates an archived contact property. The key is accepted in contact writes and renders in templates again; stored values were never removed, so they are unchanged. Unarchiving a property that is not archived returns a conflict error. tags: - email-contacts security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - command parameters: - name: property_id in: path required: true description: ID of the contact property to unarchive. schema: $ref: '#/components/schemas/ContactPropertyID' - $ref: '#/components/parameters/IdempotencyKey' responses: '200': description: The reactivated contact property. content: application/json: schema: $ref: '#/components/schemas/ContactProperty' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/Unprocessable' '429': $ref: '#/components/responses/RateLimited' '500': $ref: '#/components/responses/InternalError' '503': $ref: '#/components/responses/ServiceUnavailable' x-surfaces: - cli - make - mcp - n8n - sdk components: schemas: AudienceID: type: string minLength: 1 pattern: ^adn_[0-9a-hjkmnp-tv-z]{26}$ example: adn_01krdgeqcxet5s7t44vh8rt9mg ErrorBody: type: object additionalProperties: false required: - type - code - name - message - doc_url - request_id properties: type: type: string minLength: 1 description: Broad category for coarse client branching. enum: - auth_error - bad_request_error - billing_error - conflict_error - gone_error - internal_error - misdirected_error - not_found_error - not_implemented_error - payload_too_large_error - permission_error - precondition_error - rate_limit_error - service_unavailable_error - too_early_error - validation_error code: type: string minLength: 1 pattern: ^E\d{5}$ description: Opaque, stable, unique error identifier. Never reused. name: type: string minLength: 1 description: Human-readable slug for log readability. Paired with code, never replaces it. message: type: string minLength: 1 description: Human-readable description. Not stable; clients must not parse it. param: type: string minLength: 1 description: Identifies the offending field. Omitted when not applicable. doc_url: type: string minLength: 1 format: uri description: Stable link to the docs page for this error code. request_id: type: string minLength: 1 description: Request correlation ID for support and troubleshooting. Also returned in the `X-Request-Id` response header. vendor_code: type: string minLength: 1 description: 'Verbatim error code from an external system, such as an SMTP response code or a payment decline code. Present only when the code may help you resolve the error. ' details: type: array description: Per-field validation errors. Present only on validation_error responses. items: $ref: '#/components/schemas/ErrorDetail' remediation: type: string minLength: 1 description: A human-readable next step to resolve this error. Present when a recovery is known. next: type: array description: 'The steps that resolve this error. Perform them in order, re-reading after each; a `wait` or `terminal` step is always last. Present for errors with a well-defined recovery, such as unmet preconditions and conflicts. ' items: $ref: '#/components/schemas/NextAction' ContactUpdateRequest: type: object additionalProperties: false properties: email: type: - string - 'null' format: email maxLength: 254 description: New email address for the contact. Trimmed and lowercased before it is stored and checked for uniqueness. Must not be in use by another contact in the workspace. Omit to keep the current address; set to `null` to remove it, as long as the contact keeps at least one identifier. phone_number: type: - string - 'null' maxLength: 32 description: New phone number for the contact, in E.164 format with the leading `+` and country code. Spaces and punctuation are accepted and stripped. Stored in its canonical form, which may differ from what you send, and unique within the workspace. Omit to keep the current number; set to `null` to remove it, as long as the contact keeps at least one identifier. An empty string behaves as `null`. first_name: type: - string - 'null' maxLength: 100 description: The contact's first name. Set to `null` to clear. last_name: type: - string - 'null' maxLength: 100 description: The contact's last name. Set to `null` to clear. external_id: type: - string - 'null' maxLength: 254 description: Your own identifier for this contact. Unique within the workspace when set. Set to `null` to clear. data: type: object additionalProperties: true description: 'Custom property values to merge into the contact''s existing data. Supplied keys are set, keys with a `null` value are removed, and omitted keys remain unchanged. Each key must be an active contact property. Each value must match the property''s declared type: string, number, boolean, or RFC 3339 datetime. Strings can contain up to `500` characters. An unregistered or archived key returns a validation error. The serialized result is limited to 2 KB.' example: first_name: Alice last_name: Anderson ContactPropertyID: type: string minLength: 1 pattern: ^prp_[0-9a-hjkmnp-tv-z]{26}$ example: prp_01krdgeqcxet5s7t44vh8rt9mg ContactMatchedOn: type: - string - 'null' enum: - email - phone_number - external_id - null description: Which identifier matched a batch entry to an existing contact. `null` when the entry created a new contact. ContactUpsertResultItem: type: object additionalProperties: false required: - entry - matched_on - status properties: entry: $ref: '#/components/schemas/ContactUpsertEntry' matched_on: $ref: '#/components/schemas/ContactMatchedOn' description: Which identifier matched this entry to an existing contact. `null` when the entry created a new contact. status: type: string minLength: 1 enum: - created - updated - failed description: "What happened to this contact.\n\n- `created`: a new contact was created for the address.\n- `updated`: an existing contact with the address was updated.\n- `failed`: the entry was rejected and `error` explains why. A failed entry\n does not affect the other entries in the request.\n" contact_id: $ref: '#/components/schemas/ContactID' description: ID of the created or updated contact. Absent when the entry failed. error: $ref: '#/components/schemas/ContactUpsertError' description: Why this entry failed. Absent for successful entries. ContactUpsertResult: type: object additionalProperties: false required: - data properties: data: type: array items: $ref: '#/components/schemas/ContactUpsertResultItem' description: One entry per contact in the request, in submission order. ContactPropertyUpdateRequest: type: object additionalProperties: false properties: fallback_value: maxLength: 500 description: Default used when a contact has no value for this property and the template does not supply an inline fallback. A string, number, boolean, or RFC 3339 datetime matching the declared type (strings up to `500` characters); a value of another type returns a validation error. Set to `null` to remove the fallback. example: fallback_value: free _ListEnvelopeWithTotal: allOf: - $ref: '#/components/schemas/_ListEnvelope' - type: object properties: total: type: - integer - 'null' format: int64 minimum: 0 description: Total number of items matching the request's filters across all pages. Present only when `include_total=true` was passed; otherwise `null`. ContactPropertyCreateRequest: type: object additionalProperties: false required: - key - type properties: key: type: string minLength: 1 maxLength: 50 pattern: ^[a-z][a-z0-9_]*$ description: The property key, used as the key in contact data and as the attribute in the `bird.contact.` broadcast template variable. Lowercase letters, digits, and underscores, starting with a letter. Cannot be changed after creation. type: $ref: '#/components/schemas/ContactPropertyType' fallback_value: maxLength: 500 description: Default used when a contact has no value for this property and the template does not supply an inline fallback. A string, number, boolean, or RFC 3339 datetime matching the declared type (strings up to `500` characters), or `null` for no fallback; a value of another type returns a validation error. example: key: plan type: string fallback_value: free ContactIdentifierFilter: type: string enum: - email - phone_number description: Which identifier a contact has on file, `email` for an email address or `phone_number` for a phone number. ContactUpsertRequest: type: object additionalProperties: false required: - contacts properties: contacts: type: array minItems: 1 maxItems: 1000 items: $ref: '#/components/schemas/ContactCreateRequest' description: Contacts to create or update, matched automatically against every identifier an entry supplies. Existing contacts are updated with the fields each entry supplies; omitted fields keep their stored values, so an entry can set fields but never clear them. Unmatched entries create contacts. audience_ids: type: array minItems: 1 maxItems: 10 items: $ref: '#/components/schemas/AudienceID' description: Audiences every contact in this request is added to. Contacts that are already members are left in place. Every listed audience must exist, or the whole request fails with a validation error and nothing is written. match_on: $ref: '#/components/schemas/ContactMatchKey' description: Optional field used to match every entry to an existing contact. Every entry must include this field when set. When omitted, each entry is matched against all identifiers it supplies. No match creates a contact, one match updates it, and identifiers that match multiple contacts return an error naming each contact. data_mode: type: string enum: - merge - replace default: merge description: 'How a supplied `data` object is applied to an existing contact. The default `merge` mode adds the supplied keys to the contact''s stored custom values. A key with a `null` value deletes that key. The `replace` mode overwrites the whole stored `data` map with the supplied map. In both modes a contact that omits `data` keeps its stored values unchanged, so an import that touches one attribute never wipes the others. ' example: contacts: - email: alice@acme.com first_name: Alice last_name: Anderson - email: bob@acme.com first_name: Bob last_name: Baker audience_ids: - adn_01krdgeqcxet5s7t44vh8rt9mg ContactID: type: string minLength: 1 pattern: ^con_[0-9a-hjkmnp-tv-z]{26}$ example: con_01krdgeqcxet5s7t44vh8rt9mg _ListEnvelope: type: object required: - next_cursor - prev_cursor - refresh_cursor properties: next_cursor: type: - string - 'null' description: Cursor for the next page. Pass back as `starting_after` to advance forward. `null` when no next page exists. example: eyJ2IjoxLCJzIjoiXCIyMDI2LTA1LTI1VDE0OjAzOjEwWlwiIiwiaSI6IjAxOTJmM2IxLTRjN2UtN2EyYi05ZDYxLThmM2E1YzJlN2I0MCJ9 prev_cursor: type: - string - 'null' description: Cursor for the previous page. Pass back as `ending_before` to step backward. `null` when no previous page exists. example: null refresh_cursor: type: - string - 'null' description: Refresh anchor, the first row of this response. Pass back as `ending_before` to fetch what precedes it in the current sort order. On a newest-first sort those are the items that have appeared since; on any other sort they are the items that sort earlier, so refreshing such a list means re-fetching it instead. Non-`null` whenever `data` is non-empty; `null` only on an empty page. Distinct from `prev_cursor`. example: eyJ2IjoxLCJzIjoiXCIyMDI2LTA1LTI1VDE2OjQyOjAxWlwiIiwiaSI6IjAxOTJmM2IxLTllMDQtN2NkMy1iODE3LTJhNmY0ZDFjOGUwOSJ9 Timestamps: type: object required: - created_at - updated_at properties: created_at: type: string format: date-time minLength: 1 readOnly: true example: '2026-05-20T09:14:52Z' updated_at: type: string format: date-time minLength: 1 readOnly: true example: '2026-05-25T16:42:01Z' ContactList: allOf: - type: object required: - data properties: data: type: array description: Page of contact objects. items: $ref: '#/components/schemas/Contact' - $ref: '#/components/schemas/_ListEnvelopeWithTotal' ErrorDetail: type: object additionalProperties: false required: - param - message properties: param: type: string minLength: 1 description: 'Dotted field path, such as `to[0].email`, `subject`, or `.`. When the request was rejected for a query parameter the endpoint does not declare, this carries that parameter''s name instead of a field path. ' message: type: string minLength: 1 description: What is wrong with this field. ContactProperty: allOf: - type: object required: - id - key - type - created_at - updated_at properties: id: readOnly: true $ref: '#/components/schemas/ContactPropertyID' description: ID of the property, accepted by every operation that takes a `property_id`. key: type: string minLength: 1 maxLength: 50 pattern: ^[a-z][a-z0-9_]*$ description: The property key, used as the key in contact data and as the attribute in the `bird.contact.` broadcast template variable. Lowercase letters, digits, and underscores, starting with a letter. Cannot be changed after creation. type: $ref: '#/components/schemas/ContactPropertyType' fallback_value: maxLength: 500 description: Default used when a contact has no value for this property and the template does not supply an inline fallback. A string, number, boolean, or RFC 3339 datetime matching the declared type (strings up to `500` characters), or `null` when no fallback is set. archived: type: boolean readOnly: true description: Whether the property is archived. An archived property is rejected in new contact writes and stops rendering in templates, but every value already stored on contacts is preserved. Reactivate it with unarchive. - $ref: '#/components/schemas/Timestamps' ContactCreateRequest: type: object additionalProperties: false properties: email: type: string format: email maxLength: 254 description: The contact's email address. Trimmed and lowercased before it is stored and checked for uniqueness. Unique within the workspace. Supply an email address, a phone number, or both. phone_number: type: string maxLength: 32 description: The contact's phone number in E.164 format, including the leading `+` and country code. Spaces and punctuation are accepted and stripped; the number is stored in its canonical form, which may differ from what you send, and is unique within the workspace. An empty string is treated as if the field were omitted. Supply an email address, a phone number, or both. example: '+31612345678' first_name: type: string maxLength: 100 description: The contact's first name. last_name: type: string maxLength: 100 description: The contact's last name. external_id: type: string maxLength: 254 description: Your own identifier for this contact, such as a user ID in your system. Unique within the workspace when set. data: type: object additionalProperties: true description: 'Custom property values for this contact. Each key must be an active contact property. Each value must match the property''s declared type: string, number, boolean, or RFC 3339 datetime. Strings can contain up to `500` characters, and a `null` value is ignored. Unregistered or archived keys return a validation error. The serialized data is limited to 2 KB.' example: email: alice@acme.com phone_number: '+31612345678' first_name: Alice last_name: Anderson ContactUpsertEntry: type: object additionalProperties: false required: - email - phone_number - external_id description: The identifiers a batch entry supplied, in the normalized form used for matching. A field is `null` when the entry did not include it. These values identify the request entry and do not represent the contact's current state. properties: email: type: - string - 'null' description: Email address this entry carried, trimmed and lowercased. `null` when the entry carried none. phone_number: type: - string - 'null' description: Phone number this entry carried, in its normalized international form. `null` when the entry carried none. A row rejected for an invalid phone echoes the value as sent, trimmed, since no normalized form exists. external_id: type: - string - 'null' description: Your own identifier for this entry, when the entry supplied one. AudienceRef: type: object additionalProperties: false required: - id - name description: A compact reference to an audience, carrying its ID and display name. properties: id: readOnly: true $ref: '#/components/schemas/AudienceID' description: ID of the referenced audience. name: type: string minLength: 1 maxLength: 100 description: The audience's display name. NextAction: type: object additionalProperties: false required: - kind - description properties: kind: type: string minLength: 1 x-extensible-enum: - operation - external - wait - terminal description: "What you do about this step.\n\n- `operation`: call the operation named in `operation`, then\n read again.\n- `external`: act somewhere this API does not reach, then read\n again.\n- `wait`: nothing is asked of you, so read again later.\n- `terminal`: nothing you do resolves this, so stop retrying.\n\nTolerate a value you do not recognize: show the `description` and\noffer no action.\n" description: type: string minLength: 1 description: A short, human-readable label for the step, suitable for display. operation: type: string minLength: 1 description: 'The operationId to call. Present only when `kind` is `operation`. The operation''s own schema says how to call it; this says only which one, and what to address it with. ' params: type: object additionalProperties: type: string minLength: 1 description: 'The parameters that address the operation, by name: `{"sender_id": "…"}` for an operation on `/v1/sms/senders/{sender_id}/requirements`. A parameter the operation takes in its query string is given the same way, so an operation addressed as `?subject_id=` carries `{"subject_id": "…"}`. Every parameter the call needs is here, whether its value came from the thing you were acting on or is fixed for this step, so you can make the call from this object alone. Present only when `kind` is `operation` and the operation names a subject. A request body, when the operation takes one, is described by the operation''s own schema and never appears here. ' url: type: string format: uri description: 'A URL to open. Present only when `kind` is `external`, and only when the step has one. An external step whose `description` says to go and do something with no URL to open is normal. ' ContactMatchKey: type: string enum: - email - phone_number - external_id description: A contact identifier a batch entry can be matched on. Error: type: object additionalProperties: false required: - error properties: error: $ref: '#/components/schemas/ErrorBody' ContactUpsertError: type: object additionalProperties: false required: - type - code - message properties: type: type: string minLength: 1 description: Machine-readable error category for this entry, such as `validation_error` or `conflict_error`, in the same vocabulary as the top-level error `type`. New categories may be added over time, so treat unrecognized values as a generic failure. code: type: string minLength: 6 pattern: ^E\d{5}$ description: Specific error code for this entry, from the same catalog as the top-level error `code`. `E04058` means the entry matched two contacts and requires review. `E04055` means the phone number belongs to another contact and you must retry with different data. Both are `conflict_error` errors; the code distinguishes them. message: type: string minLength: 1 description: Human-readable explanation of why this entry failed. ContactPropertyType: type: string minLength: 1 enum: - string - number - boolean - datetime x-enum-varnames: - ContactPropertyTypeString - ContactPropertyTypeNumber - ContactPropertyTypeBoolean - ContactPropertyTypeDatetime description: 'The value type every contact must use for a property. Cannot be changed after creation. `datetime` values are RFC 3339 timestamps with an explicit offset. Examples include `2024-01-15T09:30:00Z` and `2024-01-15T11:30:00+02:00`. A bare date or a time with no offset is rejected. The value is normalized to UTC with second precision on write, so `2024-01-15T11:30:00+02:00` is stored and returned as `2024-01-15T09:30:00Z`, and any fractional seconds are dropped. ' ContactPropertyList: allOf: - type: object required: - data properties: data: type: array description: Page of contact property objects. items: $ref: '#/components/schemas/ContactProperty' - $ref: '#/components/schemas/_ListEnvelope' Contact: allOf: - type: object required: - id - email - phone_number - created_at - updated_at properties: id: readOnly: true $ref: '#/components/schemas/ContactID' description: ID of the contact, accepted by every operation that takes a `contact_id`. email: type: - string - 'null' format: email maxLength: 254 description: The contact's email address, in its stored form, trimmed and lowercased before uniqueness is checked. Unique within the workspace. `null` when the contact has no email address. phone_number: type: - string - 'null' minLength: 5 maxLength: 16 description: 'The contact''s phone number in normalized international form: a leading `+` and four to 15 digits. We normalize formatting but do not verify the number against numbering-plan metadata. The number is unique within the workspace. Because carriers recycle disconnected numbers, use `external_id` as the durable key for your own records. `null` when the contact has no phone number.' first_name: type: - string - 'null' maxLength: 100 description: The contact's first name. Available in broadcast templates as `bird.contact.first_name`. last_name: type: - string - 'null' maxLength: 100 description: The contact's last name. Available in broadcast templates as `bird.contact.last_name`. external_id: type: - string - 'null' maxLength: 254 description: Your own identifier for this contact, such as a user ID in your system. Unique within the workspace when set. data: type: object additionalProperties: true description: 'Custom property values for this contact, available in broadcast templates as `bird.contact.`. Each key is a property created via the contact properties API, and each value is a string, number, boolean, or RFC 3339 datetime matching the property''s declared type (strings up to `500` characters). Total size is capped at 2 KB serialized. Values stored under a property that was later archived remain readable here. ' audiences: type: array readOnly: true description: The audiences this contact belongs to, most-recently-joined first. Only present when listing contacts; omitted from every other contact operation. items: $ref: '#/components/schemas/AudienceRef' - $ref: '#/components/schemas/Timestamps' parameters: IncludeTotal: name: include_total in: query required: false description: When true, the response includes a `total` field with the total number of items matching the request's filters across all pages. schema: type: boolean default: false IdempotencyKey: name: Idempotency-Key in: header required: false description: "Client-supplied key. On operations supporting request deduplication, a retained\nresponse is replayed for duplicate requests with the same key within the\nidempotency window (3 hours by default). This protection requires a workspace,\norganization, or staff-account scope. User-only and unauthenticated operations,\nstreams, and operations with a separate replay contract do not use this\nresponse replay.\n\nOn a supported operation, if idempotency protection is unavailable before execution, the API returns\n`503 IdempotencyUnavailable` (E01033) without executing this attempt. Retry with\nbackoff using the same key and request. An operation that takes effect before\nits response is retained can still execute again on retry.\n\nTwo distinct 409 errors signal misuse:\n\n- `request_in_progress` (E01004): The same key is currently being\n processed by a concurrent request. Wait briefly and retry. The lock expires within 30 seconds.\n- `idempotency_key_reuse` (E01005): The same key has already completed\n against a different request body or method. Generate a new key.\n\nRecommended key format is `/` (for example `welcome-user/usr_abc123`).\n" schema: type: string maxLength: 255 StartingAfter: name: starting_after in: query required: false description: Cursor from the `next_cursor` field of a previous list response. Returns items immediately after the cursor position in the current sort order. schema: type: string EndingBefore: name: ending_before in: query required: false description: Cursor from the `prev_cursor` or `refresh_cursor` field of a previous list response. Returns items immediately before the cursor position in the current sort order. `prev_cursor` returns the preceding page. `refresh_cursor` anchors at the first row of that response, which on a newest-first sort is how to fetch the items that have appeared since. schema: type: string PaginationLimit: name: limit in: query required: false description: Maximum number of items to return per page. schema: type: integer minimum: 1 maximum: 100 default: 25 responses: InternalError: description: Internal server error content: application/json: schema: $ref: '#/components/schemas/Error' RateLimited: description: Rate limit exceeded headers: RateLimit: $ref: '#/components/headers/RateLimit' RateLimit-Policy: $ref: '#/components/headers/RateLimit-Policy' Retry-After: $ref: '#/components/headers/RetryAfter' content: application/json: schema: $ref: '#/components/schemas/Error' Forbidden: description: Insufficient permissions content: application/json: schema: $ref: '#/components/schemas/Error' ServiceUnavailable: description: 'The service is temporarily unavailable. If `Retry-After` is present, wait for that delay before retrying; otherwise, retry with exponential backoff. Reuse the same idempotency key and request when retrying a mutation. ' headers: Retry-After: $ref: '#/components/headers/RetryAfter' content: application/json: schema: $ref: '#/components/schemas/Error' Unprocessable: description: 'The request has invalid field values, violates a business rule, or carries a query parameter the endpoint does not declare. Field validation errors use `type: validation_error` and include the affected fields in `details`. Business-rule errors identify the failed rule in `type`. ' content: application/json: schema: $ref: '#/components/schemas/Error' BadRequest: description: Bad request content: application/json: schema: $ref: '#/components/schemas/Error' NotFound: description: Resource not found content: application/json: schema: $ref: '#/components/schemas/Error' Unauthorized: description: Authentication required content: application/json: schema: $ref: '#/components/schemas/Error' Conflict: description: Resource conflict content: application/json: schema: $ref: '#/components/schemas/Error' headers: RetryAfter: description: 'Number of seconds to wait before retrying the request. ' schema: type: integer minimum: 0 example: 35 IdempotencyReplay: description: The API includes this header when it replays the response for an earlier request that used the same `Idempotency-Key`. The API does not process the request again. schema: type: string enum: - 'true' RateLimit: description: 'Remaining capacity for the request''s rate-limit policy as an IETF Structured Field. Format: `"";r=;t=`. ' schema: type: string example: '"email_send";r=842;t=35' RateLimit-Policy: description: 'Effective quota for the request''s rate-limit policy as an IETF Structured Field. Format: `"";q=;w=`. ' schema: type: string example: '"email_send";q=1000;w=60' securitySchemes: BearerAuth: type: http scheme: bearer description: 'Pass the API key as a bearer token in the `Authorization` header. Keys use the format `bk_{region}_*`. The prefix identifies the region and selects the API endpoint. Official Bird SDKs and the CLI derive the region from the key. ' CookieAuth: type: apiKey in: cookie name: bird_session description: 'Session cookie set after signing in to the Bird dashboard. The cookie value is an opaque session token; no session data is stored in the cookie itself. ' RealtimeKey: type: apiKey in: header name: X-Realtime-Key description: 'The Realtime app key. Together with `X-Realtime-Secret`, it authenticates a request to the Realtime API in addition to the workspace credential. Both values come from the app''s credentials and must belong to the calling workspace. Official Bird SDKs accept the pair as client configuration. ' RealtimeSecret: type: apiKey in: header name: X-Realtime-Secret description: 'The Realtime app secret paired with `X-Realtime-Key`. The API returns the secret only when the key is created and does not store it. Create a new key and revoke the current key if you lose the secret. Official Bird SDKs accept the pair as client configuration. '