WidersApps WidersApps

WidersApps API

A REST API for your WidersApps data — contacts, conversations, orders, and bookings. JSON in, JSON out, one uniform envelope, secured with a bearer key you create yourself.

Base URL

Every endpoint lives under a single, versioned base path. Send all requests over HTTPS.

https://console.widers.net/api/v1

Resources

The core resources and the scope each needs. This is a curated starting set — the interactive Reference lists every endpoint the API exposes, with the full request and response of each.

ResourceWhat you can doScope
ContactsList · view · create · update · deletecontacts.view · contacts.manage
ProductsList · view · create + update core fields (name, price, status, stock — for catalog/inventory sync)commerce.view · commerce.manage
OrdersList · view · create · advance statuscommerce.view · commerce.manage
MessagesList · view · sendinbox.access
ConversationsList · view · assign · transfer · close · reopen · read · mute · archive · follow · note · taginbox.access
BookingsFull CRUD + availabilitycalendar.view · calendar.manage
TagsList tags; create, rename and delete your own (system tags are read-only)inbox.access · tags.manage
MediaUpload a file and get a URL to attach when sending a messageinbox.access
Quick repliesList and create saved quick repliesquick-replies.manage
CampaignsList broadcasts with their stats; pause, resume or cancel a running onecampaigns.view · campaigns.manage
Creating an order always raises it as Pending — no status is accepted on create, so revenue is never realised without payment. Advancing an order to a paid status stamps its paid_at without collecting any money (for manual, bank-transfer or cash-on-delivery flows) and is gated on commerce.manage.

Message delivery, read and failed statuses arrive as webhooks (message.delivered · message.read · message.failed) rather than as pollable endpoints. Conversation lifecycle changes (created · assigned · closed · tagged · …) are webhooks too — subscribe under Settings → Webhooks.

Create an API key

Create a key in your dashboard under Settings → Developers → API keys (Owner-only). When you create one you choose:

  • A name — so you can recognise and revoke it later.
  • Scopes — you can only grant scopes you hold yourself.
  • Live or test — a test (sandbox) key never touches real data.
  • An optional expiry.

The full key is shown once, at creation. Copy it then and store it safely — if you lose it, revoke it and create a new one.

Authentication

Authenticate every request with a bearer token in the Authorization header. Paste the key exactly as shown, including its prefix:

Authorization: Bearer wa_live_<id>|<token>

A sandbox key carries the wa_test_ prefix instead:

Authorization: Bearer wa_test_<id>|<token>
The wa_live_ / wa_test_ prefix is cosmetic — it is stripped before the token is parsed. Live vs. test mode is enforced by the key's ability, never by the prefix, so renaming a prefix cannot change what a key can do.

One user belongs to exactly one company, so a token fixes the account it acts on — there is no tenant parameter to pass.

Scopes

Scopes are the platform's own permission names, so a key can never do more than a dashboard user with the same grants. Available scopes:

ScopeDescription
contacts.view Read contacts
contacts.manage Manage contacts
inbox.access Messages (read & send)
tags.manage Manage tags
quick-replies.manage Manage quick replies
commerce.view Read orders & products
commerce.manage Manage orders & products
campaigns.view Read campaigns
campaigns.manage Control campaigns (pause, resume, cancel)
calendar.view Read bookings
calendar.manage Manage bookings

A key is minted with exactly the scopes you tick — never a wildcard, and never more than you hold yourself.

Response envelope

A successful response wraps the payload in a data key, with an optional meta block (used by paginated lists):

{
  "data": { ... },
  "meta": { "next_cursor": null, "has_more": false }
}

A failure returns a single, uniform error object:

{
  "error": {
    "type": "invalid_request_error",
    "code": "not_found",
    "message": "The requested resource was not found.",
    "param": null,
    "request_id": "req_01J..."
  }
}

Error codes

The code string is a stable, machine-readable contract (locale-independent, never renamed) so you can branch on it; the human message is translated separately. The type is the coarse error family.

CodeTypeHTTP
bad_request invalid_request_error 400
validation_failed invalid_request_error 422
unauthenticated authentication_error 401
forbidden authorization_error 403
not_found invalid_request_error 404
method_not_allowed invalid_request_error 405
not_acceptable invalid_request_error 406
conflict idempotency_error 409
idempotency_conflict idempotency_error 409
payload_too_large invalid_request_error 413
rate_limited rate_limit_error 429
server_error api_error 500

Every error echoes request_id (the same value as the X-Request-Id response header) — quote it when reporting an issue.

Rate limits

The API allows 120 requests per minute per token (unauthenticated requests are throttled by client IP). Every response carries the standard headers:

  • X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After
  • Exceeding the budget returns 429 with the rate_limited code; back off until Retry-After.

Idempotency

Send an Idempotency-Key header on any POST, PUT, PATCH, or DELETE so a retried request is applied at most once. Use a fresh UUID per logical operation:

Idempotency-Key: 6f9e0b2c-1a3d-4e5f-8a7b-9c0d1e2f3a4b
  • A replay of the same key + body returns the stored response with Idempotency-Replayed: true.
  • Keys are remembered for 24 hours.
  • Reusing a key with a different body, or while the first request is still in flight, returns 409 idempotency_conflict.

Pagination

Lists use opaque, cursor-based (keyset) pagination. Pass limit (default 25, max 100) and an opaque cursor:

GET https://console.widers.net/api/v1/contacts?limit=25&cursor=<opaque>

The response meta carries the next page cursor and whether more rows exist. When has_more is false, next_cursor is null:

"meta": { "next_cursor": "eyJ...", "has_more": true }

A malformed cursor returns 422 — never a silent full scan. Treat the cursor as an opaque blob and pass it back verbatim.

Sandbox / test mode

Test mode is carried by the key (a test key), so you never flip it mid-request. A sandbox request never touches real customers, orders, or money — it synthesises responses:

  • Every response reports its mode: X-Widers-Mode: live / X-Widers-Mode: test
  • A test-mode key short-circuits before any real side effect, so it fires no webhooks — every webhook you receive is from live activity.
  • Side-effecting services stub their external calls in test mode.

Webhook signatures

Every webhook we POST to your endpoint is signed with a timestamped HMAC in the X-Widers-Signature header, so you can verify both authenticity and freshness:

X-Widers-Signature: t=1706342400,v1=<hmac_sha256>
  • Verify against the raw, unparsed request body — compute hmac_sha256(secret, "<t>.<body>").
  • Reject any request whose timestamp is more than 300 seconds from now (replay defence).
  • Compare with a constant-time function (hash_equals) to avoid timing attacks.

A copy-paste verifier (PHP):

<?php

function widers_verify(string $secret, string $rawBody, string $header): bool
{
    // header: "t=<unix>,v1=<hex>"
    parse_str(strtr($header, ',', '&'), $parts);
    $t = (int) ($parts['t'] ?? 0);
    $sig = (string) ($parts['v1'] ?? '');

    if (abs(time() - $t) > 300) {
        return false; // outside the 300s replay window
    }

    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);

    return hash_equals($expected, $sig);
}

Your first request

List your contacts with a single authenticated request:

curl https://console.widers.net/api/v1/contacts \
  -H "Authorization: Bearer wa_live_<id>|<token>" \
  -H "Accept: application/json"

Calendar Bookings (legacy auth)

Calendar Bookings predates this API kit and keeps its own contract. Its differences are listed below.
  • It authenticates with the legacy Sanctum abilities, not the scopes above: bookings.read / bookings.write
  • It returns the default Laravel envelope, not the data/error shape: {"data":[...]}
  • It has no cursor pagination, no sandbox mode, and no idempotency support — its list is unpaginated and hard-capped.

Ready to explore every endpoint? See the API Reference