Migration and reference
REST API and OpenAPI
Inspect the versioned /v1 HTTP contract when integrating without the Node.js SDK.
What this guide covers
- Send authenticated JSON requests to the stable /v1 API.
- Use the OpenAPI contract for endpoint, schema, and response details.
- Rely on public Yanola IDs rather than internal database identifiers.
Use the published contract
Yanola publishes the complete Developer Platform REST contract as OpenAPI 3.1 JSON. It is the machine-readable source for paths, parameters, request and response schemas, authentication, examples, headers, and public error responses.
The checked-in artifact is served with the documentation site and is copied byte-for-byte from the backend source contract. Download it for local inspection, validation, mock generation, or client tooling.
- View OpenAPI 3.1 JSONOpen the published, machine-readable REST /v1 contract in your browser.
- Download OpenAPI 3.1 JSONSave the exact contract used by backend and SDK contract verification.
- Use the Node.js SDKPrefer the hand-written @yanola/whatsapp client when building a server-side Node.js integration.
The published document declares OpenAPI 3.1.0, beta contract version 1.0.0-beta.0, the production server https://api.yanola.ai/v1, and a first-class signed webhook callback contract.
Stable REST /v1 and webhook compatibility
REST /v1 will not remove or rename documented endpoints or fields, make optional request fields required, change documented field types incompatibly, reuse stable error codes with different meanings, or change accepted behavior incompatibly. A breaking protocol change requires a new major API base path.
Compatible evolution can add endpoints, optional capabilities, optional response fields, distinguishable error codes, explicitly subscribed webhook event types, and fields inside extensible normalized event data. Ignore unknown response and webhook fields. A deprecated /v1 operation remains part of /v1 even after its replacement is documented.
| Area | Stable contract | Compatible evolution |
|---|---|---|
REST /v1 | Paths, required inputs, public IDs, field meanings, error envelope, and successful behavior. | New endpoints and optional response fields. |
yanola webhook mode | Normalized Event envelope, signed headers, payload-mode identity, and public IDs. | New optional event data and explicitly subscribed event types. |
yanola_with_raw mode | Normalized envelope plus a required nullable raw field. | Retained provider content can vary; raw becomes null when unavailable or expired. |
meta webhook mode | Unwrapped object / entry / changes / value structure. | Provider-compatible value fields can be added; the body is never promised as a byte-for-byte Meta replay. |
Breaking change | Never applied in place to /v1. | Published under a new major API base path and corresponding SDK major when required. |
The top-level yanolaEvent webhook documents yanola-signature, yanola-timestamp, yanola-event-id, yanola-delivery-id, yanola-payload-format, optional yanola-replayed, all three payload schemas, at-least-once delivery, and any-2xx acknowledgement.
Make an authenticated REST request
Send every request to the production /v1 base URL over HTTPS. Put one Yanola API Key in the Authorization header using the Bearer scheme and keep that key in server-side environment configuration.
The key resolves the Yanola Account, Agent Project, environment, scopes, and optional Sender restrictions. A Test Mode key cannot read or mutate Live Mode data, and a Live Mode key cannot access Test Mode data.
curl --request POST \
--url https://api.yanola.ai/v1/messages/text \
--header "Authorization: Bearer $YANOLA_API_KEY" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: send-order-123" \
--data '{
"from": "+15557654321",
"to": "+15551234567",
"body": "Hello from Yanola",
"previewUrl": false
}'A Yanola API Key grants Agent Project access. Do not embed it in JavaScript bundles, browser storage, mobile binaries, URLs, logs, or analytics events.
Published request and response examples
These JSON blocks are exact copies of named examples in the published OpenAPI document. The SDK contract test parses the same component examples and validates them against their assigned OpenAPI schemas before exercising the backend routes.
{
"from": "+15557654321",
"to": "+15551234567",
"body": "Legacy phone clients can migrate the base path first.",
"previewUrl": false
}{
"id": "msg_0123456789ABCDEabcdeWXYZ",
"environment": "live",
"status": "queued",
"from": "+15557654321",
"to": "+15551234567",
"requestId": "req_0123456789",
"createdAt": "2026-07-21T12:00:00.000Z"
}Live sends return a queued public message ID after Yanola accepts the request. Follow message logs and Developer Events for sent, delivered, read, or failed state changes. Test Mode records a completed synthetic flow without calling Meta.
Message operations
| Operation | Purpose | Success |
|---|---|---|
POST /messages/text | Queue a text message. | 202 |
POST /messages/template | Queue an approved template message. | 202 |
POST /messages/media | Queue image, video, document, or audio from a public URL. | 202 |
POST /messages/location | Queue latitude and longitude with optional name and address. | 202 |
POST /messages/contact-card | Queue one or more WhatsApp contact cards. | 202 |
POST /messages/{messageId}/mark-read | Mark a Yanola-issued inbound message ID as read. | 200 |
GET /messages | List scoped message logs with filters and cursor pagination. | 200 |
GET /messages/{messageId} | Retrieve one scoped public message log entry. | 200 |
Catalog and media operations
| Operation | Purpose | Success |
|---|---|---|
GET /senders | List connected Senders visible to the API key. | 200 |
GET /templates | List approved templates for an E.164 Sender. | 200 |
GET /media/{mediaId} | Retrieve inbound media metadata and a short-lived signed proxy URL. | 200 |
REST /v1 accepts Yanola-issued msg_, msg_test_, med_, evt_, and del_ identifiers where documented. Raw Meta message and media IDs and internal database identifiers are not public API inputs.
Event, delivery, and usage operations
| Operation | Purpose | Success |
|---|---|---|
GET /events | List retained, normalized Developer Platform events. | 200 |
GET /events/{eventId} | Retrieve one normalized event. | 200 |
GET /webhook-deliveries | List scoped webhook delivery summaries. | 200 |
GET /webhook-deliveries/{deliveryId} | Retrieve one webhook delivery summary. | 200 |
POST /webhook-deliveries/{deliveryId}/replay | Create a new pending delivery from a failed source delivery. | 202 |
GET /usage | List immutable usage and Yanola Balance pricing snapshots. | 200 |
Use idempotency for safe mutations
Send an Idempotency-Key when your application may retry a send, mark-read, or delivery-replay request. The value must be 8 through 255 characters.
Yanola scopes the key by Agent Project, environment, concrete endpoint, API key, and request-body hash. Repeating the same operation with the same body returns the original outcome; reusing the key with a different body returns 409 idempotency_key_reused.
- Generate one stable key for one logical mutation, such as an order notification.
- Persist the key with your job or outbox record before the first network attempt.
- Reuse it only when retrying the identical endpoint and body.
- Create a new key when the intended mutation or body changes.
Paginate and filter list operations
List endpoints use opaque cursor pagination. Omit limit for the default of 20, or request 1 through 100 items. When pagination.hasMore is true, pass pagination.nextCursor unchanged as the next cursor.
Filters are endpoint-specific. Common filters include environment, from, status, created_after, and created_before. Date filters use RFC 3339 date-time strings. Do not parse, construct, or reuse a cursor with a different filter set.
curl --get "https://api.yanola.ai/v1/messages" \
--header "Authorization: Bearer $YANOLA_API_KEY" \
--data-urlencode "limit=50" \
--data-urlencode "environment=live" \
--data-urlencode "cursor=$YANOLA_NEXT_CURSOR"Distinguish protocol and account limits
Fixed request-shape limits are part of the GA protocol. Commercial capacity is account-specific and must be discovered from the current subscription entitlement and live response headers; development seed tiers are not a public price or limit promise.
| Limit | GA value or source | Integration behavior |
|---|---|---|
List pagination | 20 by default; request 1 through 100. | Follow the opaque next cursor without changing filters. |
Idempotency-Key | 8 through 255 characters. | Persist one key per logical mutation and identical body. |
Contact cards | 1 through 10 contacts per send. | Split a larger application operation deliberately. |
Webhook signature time | SDK default tolerance is five minutes. | Verify the timestamp and exact raw body before parsing. |
REST throughput | x-ratelimit-limit, remaining, reset, and retry-after. | Size concurrency from each response rather than a plan-name guess. |
Keys, endpoints, projects, Test Mode, retention, and support | Current account entitlement in Billing. | Do not hard-code seed plan names or quantities. |
Read response and rate-limit headers
| Header | Meaning | Action |
|---|---|---|
x-request-id | Yanola request ID for support and debugging. | Record it with the operation result, never with API key material. |
x-ratelimit-limit | Maximum requests in the current window. | Use it to size concurrency conservatively. |
x-ratelimit-remaining | Requests remaining in the current window. | Slow down before capacity reaches zero. |
x-ratelimit-reset | Unix timestamp when the window resets. | Schedule later work against the reset time. |
retry-after | Seconds to wait after 429 or a temporary failure. | Wait at least this long and add jitter before retrying. |
Handle the public error envelope
Every documented JSON error response uses one stable envelope. Branch on error.code for program behavior, use retryable and HTTP status to decide retry policy, and retain requestId for support correlation.
{
"error": {
"type": "rate_limit_error",
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Retry after the time in the retry-after header.",
"requestId": "req_0123456789",
"retryable": true,
"details": {}
}
}| Status | Category | Typical action |
|---|---|---|
400 | Malformed or invalid request | Correct the request before retrying. |
401 | Invalid API key | Replace or rotate the credential. |
402 | Plan Message Cap reached | wait for the next period or upgrade the Plan; replies inside the 24-hour window, free-form or template, still go out. |
403 | Missing permission or entitlement | Change the API-key scope or subscription access. |
404 | Missing or inaccessible public resource | Verify the ID and key scope without probing other projects. |
409 | Idempotency conflict | Use the original body or a new key for a new operation. |
422 | Business state prevents the operation | Follow the action in the error details; do not blindly retry. |
429 | Rate limit exceeded | Honor retry-after and add jitter. |
500 | Unexpected API failure | Retry only when safe and keep the request ID. |
503 | Temporary dependency failure | Honor retry-after and preserve idempotency. |
Understand example coverage
Every named component example has one declared contract schema. The SDK contract test fails when an example is added without a mapping, when a mapped example violates its schema, or when a real backend response violates the operation response schema.
| Examples | Validated schema | Coverage |
|---|---|---|
TextSendLegacyPhoneRequest, TextSendTypedPhoneRequest, TextSendRegularBsuidRequest, TextSendParentBsuidRequest | TextMessageSendRequest | Phone and typed-recipient text requests. |
TemplateSendLegacyPhoneRequest, TemplateSendRegularBsuidRequest | TemplateMessageSendRequest | Approved template name, language, components, and parameters. |
MediaSendLegacyPhoneRequest, MediaSendRegularBsuidRequest | MediaMessageSendRequest | Public media URL and optional caption. |
LocationSendLegacyPhoneRequest, LocationSendRegularBsuidRequest | LocationMessageSendRequest | Phone and typed-recipient locations. |
ContactCardSendLegacyPhoneRequest, ContactCardSendRegularBsuidRequest | ContactCardMessageSendRequest | Phone and typed-recipient contact cards. |
MessageAcceptedLive, TypedMessageAcceptedRegularBsuid | MessageAcceptedResponse | Queued phone and typed-recipient public message IDs. |
MalformedRequestError, InvalidApiKeyError | ErrorResponse | Parsing and authentication failures. |
InsufficientBalanceError, IdempotencyKeyReusedError | ErrorResponse | Billing and safe-retry conflicts. |
ServiceWindowRequiredError, SenderReconnectRequiredError | ErrorResponse | Non-retryable Sender and conversation state. |
RateLimitedError | ErrorResponse | Retryable capacity failure. |
SandboxQuotaExhaustedError, SandboxMessageTypeUnsupportedError | ErrorResponse | Sandbox quota and message policy failures. |
WhatsappPhoneOnlyTemplateError, WhatsappRecipientScopeMismatchError, WhatsappRouteCapabilityUnavailableError, WhatsappTemplateCapabilityUnknownError | ErrorResponse | Typed-recipient capability and scope failures. |
Generate clients carefully
Use tooling that supports OpenAPI 3.1 and JSON Schema 2020-12 semantics. Resolve local component references, preserve string formats and regex constraints, and validate generated request types before trusting generated code.
Generated clients do not replace operational behavior. Keep API keys server-side, preserve idempotency keys across retries, honor retry-after, accept additive response fields, and follow asynchronous message state through logs or events.
REST /v1 avoids breaking request and response shape changes. Additive fields are allowed, so consumers should ignore unknown response properties even when generated models expose only the fields known at generation time.
Verify the contract locally
The canonical JSON lives in the backend project. The frontend sync command republishes it byte-for-byte, the frontend check rejects drift, the backend suite verifies REST invariants and local references, and the SDK suite validates every example plus real route responses.
cd yanola.ai
npm run openapi:synccd yanola.ai
npm run openapi:check
npm run test:developer-platform -- --runTestsByPath src/app/docs/docs-structure.spec.tsx
cd ../yanola.ai.be
npx jest src/developer-platform/openapi/openapi-v1.spec.ts --config ./jest.config.cjs --runInBand
cd ../yanola.whatsapp
npm run test:contract