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.
| Resource | What you can do | Scope |
|---|---|---|
| Contacts | List · view · create · update · delete | contacts.view · contacts.manage |
| Products | List · view · create + update core fields (name, price, status, stock — for catalog/inventory sync) | commerce.view · commerce.manage |
| Orders | List · view · create · advance status | commerce.view · commerce.manage |
| Messages | List · view · send | inbox.access |
| Conversations | List · view · assign · transfer · close · reopen · read · mute · archive · follow · note · tag | inbox.access |
| Bookings | Full CRUD + availability | calendar.view · calendar.manage |
| Tags | List tags; create, rename and delete your own (system tags are read-only) | inbox.access · tags.manage |
| Media | Upload a file and get a URL to attach when sending a message | inbox.access |
| Quick replies | List and create saved quick replies | quick-replies.manage |
| Campaigns | List broadcasts with their stats; pause, resume or cancel a running one | campaigns.view · campaigns.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>
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:
| Scope | Description |
|---|---|
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.
| Code | Type | HTTP |
|---|---|---|
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)
- 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