Blink Business API
Quote, book and track deliveries from your store or system. Sandbox, scoped keys, idempotency and signed webhooks.
Quickstart
JSON over HTTPS. Money is integer sen (RM1 = 100 sen). Build against the sandbox with a test key, then swap in a live key.
- Create a test key. In the business portal → Developer → API keys → New key → mode Test. You get
blink_test_…once; store it asBLINK_KEY. Or create one on the Try it page. - Get a quote.cURL
curl -X POST https://api-kirim.159-223-134-64.sslip.io/v1/quote \ -H "Authorization: Bearer $BLINK_KEY" \ -H "Content-Type: application/json" \ -d '{"vehicle":"motorcycle","stops":[ {"address":"Bandar Sunway","lat":3.0733,"lng":101.6078}, {"address":"SS2, Petaling Jaya","lat":3.118,"lng":101.623}]}' - Create the order. Send an
Idempotency-Keyso a retried request never books twice, and your own order number asexternalRef.cURLcurl -X POST https://api-kirim.159-223-134-64.sslip.io/v1/orders \ -H "Authorization: Bearer $BLINK_KEY" \ -H "Idempotency-Key: WC-10421-create" \ -H "Content-Type: application/json" \ -d '{"vehicle":"motorcycle","paymentMethod":"credit","externalRef":"WC-10421", "stops":[{"address":"Bandar Sunway","lat":3.0733,"lng":101.6078}, {"address":"SS2, Petaling Jaya","lat":3.118,"lng":101.623, "contactName":"Aisyah","contactPhone":"0172223344"}]}' - Watch it move. Sandbox orders are driven by a simulator (assigned → at pickup → picked up → in transit → delivered, a few seconds per step) and fire real webhooks. Skip ahead with the test helper:bash
# Sandbox only (blink_test_ key): jump a test order to any status; every webhook in between fires. curl -X POST https://api-kirim.159-223-134-64.sslip.io/v1/sandbox/orders/BL3F9A21C0DE/status \ -H "Authorization: Bearer $BLINK_TEST_KEY" \ -H "Content-Type: application/json" \ -d '{"status":"delivered"}' - Receive webhooks. Add an HTTPS endpoint in the portal, pick events, and verify every request (below).
Authentication & scopes
Send a key as a bearer token: Authorization: Bearer blink_live_…. Keys belong to one business and can be named, limited to scopes, given an expiry date and an IP allowlist (IPv4 CIDR or exact addresses). Last-used time and IP are shown in the portal. Keys cannot manage other keys.
| Prefix | Kind | Use |
|---|---|---|
blink_live_ | Secret, live | Server-side. Real orders, real drivers, billed. |
blink_test_ | Secret, sandbox | Server-side. Sandbox orders only: simulated, never dispatched or billed. |
pk_live_ / pk_test_ | Publishable | Safe in web pages. Quotes and widgets only, from the key's allowed domains. |
| Scope | Allows |
|---|---|
orders:read | GET /v1/orders, /v1/orders/{code}, recipients, sandbox reads |
orders:write | Create / cancel orders, stops, bulk, plan-day, recurring, sandbox helpers |
quotes | POST /v1/quote, /v1/route/optimize (the only scope publishable keys get) |
webhooks | Manage webhook endpoints, delivery log, replay and test events |
reports | Analytics, stop reports, invoices, calendar |
A key without the needed scope gets 403 API key is missing the … scope; expired keys get 401, calls from outside the IP allowlist 403.
Sandbox
Every business has a sandbox on the production API. Orders created with a blink_test_ key are priced like real ones and flagged sandbox: true, but they are never offered to drivers, never posted to your ledger or invoices, and never counted in analytics or ops boards. A fake rider moves them through every status and attaches a proof of delivery; webhooks fire to endpoints whose mode is test or all, with data.sandbox: true and header X-Blink-Mode: test.
POST /v1/sandbox/orders/{code}/status with {"status":"delivered"|"failed"|"cancelled"|…} forces a status (and pauses the simulator for that order). Listing with a test key returns sandbox orders only. externalRef is unique per mode, so tests can reuse real order numbers.
Idempotency
Every POST and PATCH under /v1 accepts an Idempotency-Key header (1–255 printable characters). The first response is stored for 24 hours, per key holder:
| Repeat with… | Result |
|---|---|
| Same key, same body | The stored status and body, plus Idempotent-Replayed: true. |
| Same key, different body | 422 — use a new key for a new request. |
| Same key while the first is still running | 409 — retry shortly. |
After a 5xx | Not stored: retry with the same key. |
externalRef on orders remains a second, permanent guard: a repeated ref returns the existing order with duplicate: true.
Webhooks
Add up to 20 endpoints, each with its own secret, event filter (order.*, stop.completed, …) and mode. Failed deliveries retry with exponential backoff (30 s, 1 min, 2 min … up to 1 day) and are dead-lettered after the endpoint's max attempts (default 8); replay any delivery from the log. See the event catalogue.
import { createHmac, timingSafeEqual } from "node:crypto";
// v2: X-Blink-Signature-V2: t=<unix seconds>,v1=<hex HMAC-SHA256("<t>.<raw body>", secret)>
export function verifyBlink(rawBody: string, header: string, secret: string, toleranceS = 300) {
const m = /t=(\d+),v1=([0-9a-f]+)/.exec(header ?? "");
if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > toleranceS) return false;
const expected = createHmac("sha256", secret).update(`${m[1]}.${rawBody}`).digest("hex");
return expected.length === m[2].length && timingSafeEqual(Buffer.from(expected), Buffer.from(m[2]));
}
// express: app.post("/blink/webhook", express.raw({ type: "application/json" }), (req, res) => {
// if (!verifyBlink(req.body.toString("utf8"), req.header("x-blink-signature-v2"), process.env.BLINK_WEBHOOK_SECRET)) return res.sendStatus(401);
// const { event, data } = JSON.parse(req.body); res.sendStatus(200);
// });SDKs & plugins
Zero-dependency SDKs live in sdks/: TypeScript (fetch), PHP (curl) and Python (stdlib). Each sets idempotency keys and verifies webhook signatures. The WooCommerce plugin shows live rates at checkout and books on processing. Embeddable fee and booking widgets use publishable keys.
Errors
| Status | Meaning |
|---|---|
| 400 | {"error":"validation","issues":[…]} — fix the request. |
| 401 | Missing, invalid, revoked or expired key. |
| 402 | Credit limit exceeded. |
| 403 | Missing scope, IP not allowed, or domain not allowed (publishable keys). |
| 404 | Not found or not yours. |
| 409 | Not allowed in the current state, or the same Idempotency-Key is in flight. |
| 422 | Business rule failed, or Idempotency-Key reused with a different body. |
| 429 | Rate limited — back off and retry. |
Machine-readable contract: openapi.yaml (OpenAPI 3.1), browsable in the reference.