openapi: 3.2.0 info: title: Bird Whatsapp Business Accounts 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: whatsapp-business-accounts description: Read the WhatsApp Business Accounts your workspace has connected, so a template can be created on the account you choose. paths: /v1/whatsapp/business-accounts: get: operationId: listWhatsAppBusinessAccounts x-snippet-key: whatsapp.business_accounts.list summary: List WhatsApp Business Accounts description: 'Returns a paginated list of the WhatsApp Business Accounts your workspace has connected, so you can choose which one a template belongs to. Only accounts whose setup finished are listed: an account appears once WhatsApp has reported its name and at least one of its phone numbers has finished connecting. Page through the full set with the cursors the response returns. Each account also carries the state WhatsApp last reported for it. That covers its own status, how far WhatsApp''s review of it has got, whether Meta has verified the business behind it, the Meta business portfolio that owns it, and `ban` on an account WhatsApp has banned. These are the same fields Get a WhatsApp Business Account returns, and that operation documents them.' tags: - whatsapp-business-accounts security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - dashboard - command parameters: - name: sort in: query required: false description: Field to sort by. schema: $ref: '#/components/schemas/WhatsAppBusinessAccountSortField' - $ref: '#/components/parameters/OrderDesc' - $ref: '#/components/parameters/PaginationLimit' - $ref: '#/components/parameters/StartingAfter' - $ref: '#/components/parameters/EndingBefore' responses: '200': description: A page of the WhatsApp Business Accounts your workspace has connected. content: application/json: schema: $ref: '#/components/schemas/WhatsAppBusinessAccountList' '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 - sdk /v1/whatsapp/business-accounts/{business_account_ref}: parameters: - name: business_account_ref in: path required: true description: 'WhatsApp Business Account ID (`waa_` prefix) or the WhatsApp Business Account ID Meta reports in `waba`. A value that parses as a valid ID resolves by ID; any other value resolves as Meta''s ID. ' schema: type: string minLength: 1 maxLength: 64 example: '102290129340001' get: operationId: getWhatsAppBusinessAccount x-snippet-key: whatsapp.business_accounts.get summary: Get a WhatsApp Business Account description: 'Returns one WhatsApp Business Account your workspace has connected, addressed by either the `id` the account list reports (`waa_` prefix) or the `waba` value WhatsApp reports for it. Both forms resolve to the same account. Only accounts whose setup finished can be read: an account is readable once WhatsApp has reported its name and at least one of its phone numbers has finished connecting. An account the list hides is `404` here too, in either form. The account carries the state WhatsApp last reported for it: its own status, how far WhatsApp''s review of it has got, whether Meta has verified the business behind it, and the Meta business portfolio that owns it. An account WhatsApp has banned carries `ban`, with an `appeal_url` to Meta Business Support once Bird knows the account''s portfolio. `ban` is what WhatsApp announced on its own notification, not part of the reading `meta_synced_at` dates, because WhatsApp reports a ban''s state and timing nowhere else. It is absent on an account in good standing and on one whose ban Bird was never told about, so `status` is what says whether an account can send.' tags: - whatsapp-business-accounts security: - BearerAuth: [] - CookieAuth: [] x-audiences: - public - dashboard - command responses: '200': description: The WhatsApp Business Account. content: application/json: schema: $ref: '#/components/schemas/WhatsAppBusinessAccount' '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 - sdk components: schemas: WhatsAppBusinessAccountReviewStatus: type: string minLength: 1 x-extensible-enum: - approved - deferred - pending - rejected description: 'How far WhatsApp''s own review of this WhatsApp Business Account has got. `deferred` is WhatsApp postponing the review rather than refusing it. Values are WhatsApp''s own tokens, lower-cased. Open enum: treat an unrecognized value as a review state WhatsApp added rather than as an error.' example: approved 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' WhatsAppBusinessVerificationStatus: type: string minLength: 1 x-extensible-enum: - expired - failed - ineligible - not_verified - pending - pending_need_more_info - pending_submission - rejected - revoked - verified description: 'Whether Meta has verified the business behind this WhatsApp Business Account. Verification is one of the paths to a higher messaging limit, so a value other than `verified` is often the reason a limit has not moved. Values are Meta''s own tokens, lower-cased. Open enum: treat an unrecognized value as a state Meta added rather than as an error.' example: verified WhatsAppBusinessAccountID: type: string minLength: 1 pattern: ^waa_[0-9a-hjkmnp-tv-z]{26}$ example: waa_01krdgeqcxet5s7t44vh8rt9mg WhatsAppBusinessAccount: type: object additionalProperties: false required: - id - waba - name - status - created_at - updated_at properties: id: allOf: - $ref: '#/components/schemas/WhatsAppBusinessAccountID' readOnly: true description: Unique identifier for the WhatsApp Business Account. waba: type: string minLength: 1 readOnly: true description: 'Meta''s own identifier for this WhatsApp Business Account. This is the value to send when creating a template on the account. ' example: '102290129340398' name: type: string minLength: 1 readOnly: true description: The account's name, as WhatsApp reports it. example: Acme Inc status: allOf: - $ref: '#/components/schemas/WhatsAppBusinessAccountStatus' readOnly: true description: WhatsApp's own state for this account as of `meta_synced_at`. The status is `active` until WhatsApp reports otherwise. WhatsApp already considers an account usable if Bird could connect a number under it. The absence of a reading is therefore not evidence of another state. account_review_status: allOf: - $ref: '#/components/schemas/WhatsAppBusinessAccountReviewStatus' readOnly: true description: How far WhatsApp's review of this account had got as of `meta_synced_at`. Absent until WhatsApp has reported it. business_verification_status: allOf: - $ref: '#/components/schemas/WhatsAppBusinessVerificationStatus' readOnly: true description: Whether Meta had verified the business behind this account as of `meta_synced_at`. Absent until Meta has reported it. marketing_messages_onboarding_status: allOf: - $ref: '#/components/schemas/WhatsAppBusinessAccountMarketingMessagesStatus' readOnly: true description: 'Whether this account can use WhatsApp''s Marketing Messages API, as of `meta_synced_at`. Absent until WhatsApp has reported it. Distinct from the owning portfolio''s `marketing_messages_onboarding_status` (`portfolio.marketing_messages_onboarding_status`), which Meta gives the same field name but a different vocabulary: this one is the account''s own eligibility, that one is the portfolio''s Terms-of-Service progress.' portfolio: allOf: - $ref: '#/components/schemas/WhatsAppBusinessPortfolio' readOnly: true description: The Meta business portfolio that owns this account. Absent until Meta has reported it. The portfolio is where a messaging limit is set, so every account it owns shares one. ban: allOf: - $ref: '#/components/schemas/WhatsAppBusinessAccountBan' readOnly: true description: WhatsApp's ban on this account, absent unless Bird was told of one. `status` is what the account said when Bird last read it; this is what WhatsApp announced, which arrives only on the webhook that announces it and is never re-read. meta_synced_at: type: string format: date-time minLength: 1 readOnly: true description: When Bird last read this account's state from WhatsApp. `status`, `account_review_status`, `business_verification_status`, `marketing_messages_onboarding_status` and `portfolio` are all that reading rather than live values; Bird re-reads roughly hourly. Absent for an account Bird has never read back. created_at: type: string format: date-time minLength: 1 readOnly: true description: When this account was connected. updated_at: type: string format: date-time minLength: 1 readOnly: true description: When this account was last changed. WhatsAppBusinessAccountStatus: type: string minLength: 1 x-extensible-enum: - active description: WhatsApp's own state for a WhatsApp Business Account. Values are WhatsApp's own tokens, lower-cased. This enum is open because WhatsApp documents the field in neither its API reference nor its machine-readable schema. The `active` value is the only value in WhatsApp's example response, so it is the only one Bird can name. Treat anything else as a state WhatsApp reports and this list has not caught up with. example: active WhatsAppBusinessPortfolioMarketingMessagesStatus: type: string minLength: 1 x-extensible-enum: - not_started - request_sent - term_of_service_signed description: 'How far the business portfolio has got through Meta''s Marketing Messages terms of service. - `not_started`: the portfolio has not begun the process. - `request_sent`: a request is in. - `term_of_service_signed`: the terms are accepted. A portfolio property, so every account the portfolio owns reports the same value. Distinct from the account''s own marketing-messages status, which Meta confusingly gives the same name. Values are Meta''s own tokens, lower-cased. Open enum: treat an unrecognized value as a state Meta added rather than as an error. ' example: not_started WhatsAppBusinessAccountList: allOf: - type: object required: - data properties: data: type: array description: The WhatsApp Business Accounts your workspace has connected. items: $ref: '#/components/schemas/WhatsAppBusinessAccount' - $ref: '#/components/schemas/_ListEnvelope' WhatsAppBusinessPortfolio: type: object additionalProperties: false readOnly: true required: - meta_id description: 'The Meta business portfolio that owns a WhatsApp Business Account. Bird holds no resource of its own for a portfolio, which is why the identifier is named `meta_id`: it is meaningful only against Meta''s own tools, and it is not a Bird identifier.' properties: meta_id: type: string minLength: 1 readOnly: true description: Meta's identifier for the portfolio. Treat it as an opaque string. example: '178563218361309' name: type: string minLength: 1 readOnly: true description: The portfolio's name, as Meta reports it. Absent when Meta returned none. example: Acme Holdings marketing_messages_onboarding_status: allOf: - $ref: '#/components/schemas/WhatsAppBusinessPortfolioMarketingMessagesStatus' readOnly: true description: 'How far this portfolio has got through Meta''s Marketing Messages terms of service. Absent until Meta has reported it. Distinct from the account''s own `marketing_messages_onboarding_status`, which Meta gives the same field name but a different vocabulary: that one is the account''s own eligibility, this one is the portfolio''s Terms-of-Service progress.' WhatsAppBusinessAccountMarketingMessagesStatus: type: string minLength: 1 x-extensible-enum: - eligible - onboarded description: Whether this account can use WhatsApp's Marketing Messages API. `eligible` means WhatsApp would accept an onboarding request for it; `onboarded` means it has already been onboarded. Values are WhatsApp's own tokens, lower-cased. Open enum out of necessity. WhatsApp's onboarding guide names these two values and defers the rest to an API reference that does not document the field. Treat anything else as a state WhatsApp reports that this list has not caught up with. example: onboarded _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 WhatsAppBusinessAccountBan: type: object additionalProperties: false readOnly: true required: - state - occurred_at description: WhatsApp's ban on this account, as WhatsApp announced it. Absent when there is no ban, and also when there is one WhatsApp announced before Bird began recording bans, or whose notification never reached Bird, since WhatsApp does not replay them. This is what WhatsApp announced rather than the account's current state, so it is never the field to read to decide whether an account can send. properties: state: allOf: - $ref: '#/components/schemas/WhatsAppBusinessAccountBanState' readOnly: true occurred_at: type: string format: date-time minLength: 1 readOnly: true description: When WhatsApp reported the ban, by WhatsApp's own clock. Bird can learn of a ban later than this, so it is not when Bird recorded it. example: '2026-04-10T09:12:00Z' appeal_url: type: string format: uri readOnly: true description: Where to appeal WhatsApp's decision with Meta Business Support, because neither Bird nor this API can lift one. Absent when Bird does not know the account's Meta business portfolio, since there is no support-home path to build without one. example: https://business.facebook.com/business-support-home/178563218361309/102290129340398 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. WhatsAppBusinessAccountBanState: type: string minLength: 1 enum: - disabled - scheduled_for_disable description: "Whether WhatsApp has disabled a WhatsApp Business Account or scheduled it to be\ndisabled:\n\n- `disabled`: WhatsApp has disabled the account, and it cannot send.\n- `scheduled_for_disable`: WhatsApp has set a date to disable the account, which can\n still send until then.\n\nAn account WhatsApp has reinstated reports no `ban` at all rather than a third value\nhere.\n" example: disabled 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. ' WhatsAppBusinessAccountSortField: type: string enum: - created_at default: created_at description: Sortable fields for a WhatsApp Business Account list. Error: type: object additionalProperties: false required: - error properties: error: $ref: '#/components/schemas/ErrorBody' SortOrder: type: string enum: - asc - desc description: Sort direction, ascending or descending. 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' 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' 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' headers: RetryAfter: description: 'Number of seconds to wait before retrying the request. ' schema: type: integer minimum: 0 example: 35 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' parameters: 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 OrderDesc: name: order in: query required: false description: 'Sort direction. Defaults to `desc`, which sorts from newest to oldest or largest to smallest, depending on the selected sort field. ' schema: $ref: '#/components/schemas/SortOrder' default: desc 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. '