Developer platform · API v1

Connect your website to WingsFlow

Use a server-side API key to synchronize contacts, submit business events and follow WhatsApp outcomes. The same contract works across industries and website frameworks.

1. Obtain scoped credentials

Your administrator binds an integration to one workspace and environment, then grants scopes, channels and templates. Store its key in your website backend secret store. Never put it in browser JavaScript, URLs or source control.

Use Authorization: Bearer …. Dashboard cookies do not authenticate the public API. Keys expire after 90 days; rotate before expiry. Planned rotation allows up to 24 hours of overlap. Revoke a compromised key immediately.

2. Confirm identity and grants

Call GET /me, then discover your allowed channels and approved templates. An administrator must fix missing grants; another workspace’s IDs never expand your access.

curl "https://alq4gr135m2h40f7fpmw5e9n.77.37.45.251.sslip.io/v1/me" \
  -H "Authorization: Bearer $WINGSFLOW_API_KEY"

Staging base: https://alq4gr135m2h40f7fpmw5e9n.77.37.45.251.sslip.io/v1

3. Commit website changes, then submit events

Write the website transaction and a local outbox entry together. Your backend retries the outbox through WingsFlow. Generic event names include order.created, customer.created, customer.updated, payment.success and payment.failed; validate each event’s exact schema in OpenAPI. A payment event must follow your backend’s payment verification.

Use a stable source_event_id and correlation ID. A configured automation binding determines the permitted channel and template. An accepted event does not guarantee a message: inspect its status and outcomes. An order or contact record never implies WhatsApp consent.

For direct message requests, use an approved template, a granted channel, valid consent and a stable client_reference. A queued response is not a delivery receipt. Treat unknown as requiring reconciliation; do not blindly send again.

Retries and idempotency

Every mutation requires Idempotency-Key. Keep the same key and body when retrying a logical operation. Reusing a key with different data returns 409 idempotency_conflict. Follow Retry-After for in-progress requests and rate limits.

Ordinary response replay lasts seven days. Secret-bearing webhook create/rotation responses are recoverable for one hour; after that, use the documented rotation recovery procedure. Retired message/event identifiers cannot be reused to create a second send.

Read the structured error’s code, request_id and retryable. Record request/correlation IDs without logging authorization headers or message content.

Limits and evolution

Initial ceilings are 60 requests/minute/key, 600/minute/workspace and 10 admitted sends/minute/channel. Plan/provider limits can be lower. Lists use signed cursors and a maximum page size of 100.

Business operations use the /v1 path. Events carry schema_version: "1". Breaking changes require a new major contract; clients should tolerate additive response fields while validating supported event variants.

Campaign management, contact collection listing, AI and provider setup are outside the 17-operation pilot contract. An absent endpoint returns 404; do not infer dashboard URLs are public APIs.

4. Receive signed status webhooks

Create a subscription for the exact event names you need. Save the returned signing secret in your receiver, then enable the subscription with its current If-Match version and a new idempotency key. Your endpoint must answer the signed endpoint.verification challenge with JSON {"challenge":"the-received-nonce"}. Business events begin after verification.

Compute HMAC-SHA256 over timestamp.delivery_id.event_id. followed by the untouched request body bytes. Use the complete issued whsec_… string as the key. Compare signatures in constant time, accept timestamps only within ±300 seconds, and validate the expected tenant, schema and matching body/header event ID.

Persist the event in a durable inbox before returning 2xx. Deduplicate by tenant and event ID for at least 14 days; duplicates must not repeat business effects. Events may arrive out of order: compare resource versions and reconcile through public GET operations. The envelope includes correlation and causation IDs.

Automatic delivery uses at most nine attempts over 72 hours. Transport failures, 408, 429 and 5xx retry with backoff; redirects and other 4xx terminate, while 410 disables the subscription. Inspect delivery logs and explicitly replay eligible failed/dead-letter deliveries within seven days. A delivery replay never triggers another WhatsApp message.

Planned signing-secret rotation has a 24-hour overlap; emergency rotation has none. Accept all valid active signatures during overlap. Keep TLS, clock synchronization, durable storage and receiver monitoring operational.

Quick start: your first order notification

  1. Use a test connection with contacts, consent, events, messages and template permissions restricted to your business number.
  2. Open Connect your website, select Order confirmation, map its fields to an approved template and check the sample.
  3. Call GET /v1/me from your backend. Upsert a customer identity and record actual permission evidence; an order alone is not permission.
  4. Commit the order and an outbox entry together. Send the event once with a stable source_event_id and Idempotency-Key.
  5. Read the event result and message IDs. Verify a signed callback or read message status before marking the notification delivered.

Never call both the event and direct message paths for the same notification. Keep the original event body unchanged on retries.

Clients are server-side helpers. They never retry a mutation with a new key or follow redirects with your credentials. Set the base URL to your verified WingsFlow deployment.

Shopify and WooCommerce backend adapters

Download the commerce adapter and Node client into your merchant backend. The adapter verifies raw webhook signatures, persists receipts before acknowledgement and uses stable event identities in a durable SQLite outbox. It never assumes that buying something gives WhatsApp permission.

  1. Configure one provider, store and signing secret per adapter database. Use Node 24 and persistent storage.
  2. Provide an authenticated readOrder(orderId) function. It must fetch the current order from your store API. Shopify data should be mapped into the adapter’s documented order fields; WooCommerce uses its order resource.
  3. Provide resolveContact(order) to return the WingsFlow contact with separately recorded permission. Store credentials only on the backend.
  4. Pass untouched request bytes and headers to accept(). Return 2xx only after it completes. Bound request bodies to 2 MiB before collecting them.
  5. Run drain() in a worker. Keep pending work on disk; inspect review failures before retrying. An admitted event still needs status verification.

Shopify supports order, paid-order, verified fulfilment and refund transaction updates. WooCommerce supports order, verified paid-order and failed-payment updates; custom shipping/refund extensions can submit the corresponding public event recipes. The Node downloads are backend adapter templates. An installable WooCommerce ZIP is also available: upload it under WordPress Plugins, open WooCommerce → WhatsApp notifications, and connect your scoped API key. The plugin adds optional checkout consent and handles order, payment and verified refund events. Marketplace review and actual-store qualification are separate. Qualify each actual store and its consent mapping before live traffic.

Explore the API contract

GET /me

P0. Any valid API key. Read is tenant and grant scoped.

Parameters
[]
Request body
null
Responses
{
  "200": {
    "description": "Success.",
    "headers": {
      "X-Request-Id": {
        "schema": {
          "type": "string",
          "maxLength": 180
        }
      },
      "X-RateLimit-Limit": {
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "X-RateLimit-Remaining": {
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "X-RateLimit-Reset": {
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "content": {
      "application/json": {
        "schema": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "data": {
              "$ref": "#/components/schemas/Identity"
            },
            "request_id": {
              "type": "string",
              "maxLength": 180
            }
          },
          "required": [
            "data",
            "request_id"
          ]
        },
        "example": {
          "data": {
            "tenant_id": "11111111-1111-4111-8111-111111111111",
            "integration_id": "11111111-1111-4111-8111-111111111111",
            "environment": "test",
            "scopes": [],
            "allowed_channel_ids": [],
            "allowed_template_ids": [],
            "capabilities": []
          },
          "request_id": "example"
        }
      }
    }
  },
  "400": {
    "description": "Structured 400 error. See error catalog and retry rules.",
    "headers": {
      "X-Request-Id": {
        "schema": {
          "type": "string",
          "maxLength": 180
        }
      },
      "X-RateLimit-Limit": {
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "X-RateLimit-Remaining": {
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "X-RateLimit-Reset": {
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "Retry-After": {
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      }
    },
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/Error"
        },
        "example": {
          "error": {
            "code": "invalid_request",
            "message": "Request rejected.",
            "request_id": "req_example",
            "retryable": false
          }
        }
      }
    }
  },
  "401": {
    "description": "Structured 401 error. See error catalog and retry rules.",
    "headers": {
      "X-Request-Id": {
        "schema": {
          "type": "string",
          "maxLength": 180
        }
      },
      "X-RateLimit-Limit": {
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "X-RateLimit-Remaining": {
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "X-RateLimit-Reset": {
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "Retry-After": {
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      }
    },
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/Error"
        },
        "example": {
          "error": {
            "code": "invalid_api_key",
            "message": "Request rejected.",
            "request_id": "req_example",
            "retryable": false
          }
        }
      }
    }
  },
  "403": {
    "description": "Structured 403 error. See error catalog and retry rules.",
    "headers": {
      "X-Request-Id": {
        "schema": {
          "type": "string",
          "maxLength": 180
        }
      },
      "X-RateLimit-Limit": {
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "X-RateLimit-Remaining": {
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "X-RateLimit-Reset": {
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "Retry-After": {
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      }
    },
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/Error"
        },
        "example": {
          "error": {
            "code": "invalid_request",
            "message": "Request rejected.",
            "request_id": "req_example",
            "retryable": false
          }
        }
      }
    }
  },
  "404": {
    "description": "Structured 404 error. See error catalog and retry rules.",
    "headers": {
      "X-Request-Id": {
        "schema": {
          "type": "string",
          "maxLength": 180
        }
      },
      "X-RateLimit-Limit": {
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "X-RateLimit-Remaining": {
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "X-RateLimit-Reset": {
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "Retry-After": {
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      }
    },
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/Error"
        },
        "example": {
          "error": {
            "code": "invalid_request",
            "message": "Request rejected.",
            "request_id": "req_example",
            "retryable": false
          }
        }
      }
    }
  },
  "429": {
    "description": "Structured 429 error. See error catalog and retry rules.",
    "headers": {
      "X-Request-Id": {
        "schema": {
          "type": "string",
          "maxLength": 180
        }
      },
      "X-RateLimit-Limit": {
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "X-RateLimit-Remaining": {
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "X-RateLimit-Reset": {
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "Retry-After": {
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      }
    },
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/Error"
        },
        "example": {
          "error": {
            "code": "invalid_request",
            "message": "Request rejected.",
            "request_id": "req_example",
            "retryable": true
          }
        }
      }
    }
  },
  "500": {
    "description": "Structured 500 error. See error catalog and retry rules.",
    "headers": {
      "X-Request-Id": {
        "schema": {
          "type": "string",
          "maxLength": 180
        }
      },
      "X-RateLimit-Limit": {
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "X-RateLimit-Remaining": {
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "X-RateLimit-Reset": {
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "Retry-After": {
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      }
    },
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/Error"
        },
        "example": {
          "error": {
            "code": "invalid_request",
            "message": "Request rejected.",
            "request_id": "req_example",
            "retryable": false
          }
        }
      }
    }
  },
  "503": {
    "description": "Structured 503 error. See error catalog and retry rules.",
    "headers": {
      "X-Request-Id": {
        "schema": {
          "type": "string",
          "maxLength": 180
        }
      },
      "X-RateLimit-Limit": {
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "X-RateLimit-Remaining": {
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      },
      "X-RateLimit-Reset": {
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "Retry-After": {
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      }
    },
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/Error"
        },
        "example": {
          "error": {
            "code": "invalid_request",
            "message": "Request rejected.",
            "request_id": "req_example",
            "retryable": true
          }
        }
      }
    }
  }
}

Schema references resolve in the downloadable OpenAPI document. No credentials or live requests are sent by this explorer.

Practice failure handling

This is an offline response simulator. It does not contact a provider, validate your connection or send messages.

HTTP 202 · Message accepted into the queue; not yet delivered.Next: Save its ID, then follow signed callbacks or GET /messages/{id}.

API changes and compatibility

8 October 2026: guided notification mapping, privacy-safe request logs, workspace admission ceilings, downloadable clients and developer tools added. Public API remains v1 with the existing 17 operations.

Existing response fields and logical identities remain stable. Additive fields are permitted; clients should ignore fields they do not use. Breaking changes require a new major API version and a published migration guide. No API sunset is scheduled in this release.

Keep test and live keys separate. Read connection status and expiry in your workspace before enabling live traffic. Diagnostics show at most 100 recent records and do not expose API secrets.

Pilot endpoint reference

The downloadable OpenAPI contract defines exact bodies, enums, success/error responses, conditional headers and examples. Paths below are relative to /v1.

MethodPathRequired scopes
GET/meAny valid integration key
GET/channelschannels:read
PUT/contact-identities/{external_id}contacts:write
GET/contacts/{contact_id}contacts:read
POST/contacts/{contact_id}/consentsconsents:write
GET/templatestemplates:read
GET/templates/{template_id}templates:read
POST/messagesmessages:write
GET/messages/{message_id}messages:read
POST/eventsevents:write
GET/events/{event_id}events:read
GET/webhook-subscriptionswebhooks:read
POST/webhook-subscriptionswebhooks:manage
PATCH/webhook-subscriptions/{subscription_id}webhooks:manage
POST/webhook-subscriptions/{subscription_id}/rotate-secretwebhooks:manage
GET/webhook-deliverieswebhooks:read
POST/webhook-deliveries/{delivery_id}/replaywebhooks:manage

Standalone integration examples

These files use the public contract and your credentials. They do not import WingsFlow application modules or access its database.

For the receiver, download both receiver files and openapi.json into one directory, install ajv and ajv-formats, set RECEIVER_DATABASE, RECEIVER_TENANT_ID and RECEIVER_SECRETS_JSON, then run node durable-receiver.mjs. It listens on loopback port 8788 by default. Put it behind your HTTPS proxy and use durable storage. The sample is for a single host; qualify your own backups, retention and deployment before live traffic.