Partner API, v1

Put a real queue inside your product.

Read a venue's live queue, book slots for your customers, and get a signed webhook the moment a ticket changes state. OAuth, REST, JSON, and a mock server you can run before we have met.

Quickstart

Run the mock server, get a token, read a queue. Nothing to sign up for.

# 1. run the mock (Node 18+, no dependencies)
node eqms-api-mock.js

# 2. the venue admin authorises your app. The mock approves instantly.
curl -s "http://localhost:8787/oauth/authorize?client_id=app_demo\
&venue_id=venue_sss_qc&scope=venue:read%20queue:read%20bookings:write"

# 3. exchange the code for a token
curl -s -X POST http://localhost:8787/oauth/token \
  -d grant_type=authorization_code \
  -d code=code_... -d client_id=app_demo -d client_secret=secret_demo

# 4. read the queue
curl -s http://localhost:8787/v1/venues/venue_sss_qc/queue \
  -H "Authorization: Bearer at_..."
{
  "venue_id": "venue_sss_qc",
  "as_of": "2026-09-17T10:14:22.031Z",
  "open": true,
  "lanes": [
    { "lane": "GENERAL",  "waiting": 34, "now_serving": ["G-1524"],
      "estimated_wait_seconds": 2280, "confidence": "normal" },
    { "lane": "PRIORITY", "waiting": 3,  "now_serving": ["P-41"],
      "estimated_wait_seconds": 420,  "confidence": "normal" }
  ]
}
Display as_of

Every queue response is stamped. Show that timestamp next to any number you take from it. A wait time shown as live when it is ten minutes old sends someone to a branch on bad information, and they blame the venue rather than your cache.

Sandbox and mock server

Two ways to develop without touching a real branch.

EnvironmentBase URLWhat it is
Mockhttp://localhost:8787 A single file you run yourself. Four seeded venues, one per sector, with a queue that moves. In-memory: restart and it resets.
Sandboxsandbox.api.e-qms.io Hosted, same contract, credentials issued without a sales conversation.
Productionapi.e-qms.io Real venues. Each one authorises your app itself.

Mock credentials are app_demo / secret_demo. Venues are venue_sss_qc, venue_clinic_bt, venue_lgu_psg and venue_bank_mkt. GET / prints the quickstart, and GET /_deliveries shows what the mock tried to POST to your webhook endpoint, which helps while you are still getting signature checking right.

Authentication

OAuth 2.0 authorisation code with PKCE. Three things are worth understanding before you write the code.

A venue grants access, not us

You send the venue's administrator to our authorisation page. They see your app name, their venue, and every scope you asked for. They approve, and you get tokens for that one venue.

If you serve forty clinics, you hold forty authorisations. That is deliberate. One leaked token exposes one venue, and a clinic that leaves you can revoke its own access without anybody else noticing.

Tokens

TokenLifetimeCan do
Access1 hourEverything its scopes allow, for its venue
RefreshUntil revokedMint new access tokens
client_credentials1 hour Only GET /venues, to list who has authorised you. It cannot read a queue.

Scopes

ScopeGrants
venue:readProfile, hours, services, documents
queue:readLive queue and ticket status
bookings:writeCreate and cancel bookings
tickets:writeIssue walk-in tickets, where the venue allows it
events:subscribeRegister webhook endpoints
analytics:readAggregate statistics

Ask only for what you use. A venue administrator reading a consent screen that requests everything is a venue administrator who says no.

Revocation is instant

A venue can revoke your app from its Integrations page at any moment, and both tokens stop working within seconds. Handle 401 by re-authorising, not by retrying in a loop. That page also shows when your app last called, so a forgotten integration quietly reading their data every minute is visible to them.

Endpoint reference

Base path /v1. All responses are JSON. The full machine-readable contract is in eqms-openapi.yaml.

EndpointScopeNotes
GET/venues any Venues that authorised your app
GET/venues/{id} venue:readProfile, hours, timezone, capabilities
GET/venues/{id}/services venue:read Includes the required documents. Show these before the customer travels.
GET/venues/{id}/queue queue:readLive state. Cached 5s, rate limited.
GET/venues/{id}/availability venue:readBookable slots for a service and date
POST/venues/{id}/bookings bookings:writeRequires Idempotency-Key
GET/bookings/{id} bookings:writeRead one back
DELETE/bookings/{id} bookings:writeCancel and return the slot
POST/venues/{id}/tickets tickets:writeWalk-in. Off unless the venue enables it.
GET/tickets/{id} queue:readState and advisory position
POST/tickets/{id}/notes bookings:writeAttach your own case reference
POST/venues/{id}/webhooks events:subscribeRegister an endpoint
GET/venues/{id}/stats analytics:readAggregates including the wasted-trip rate

Booking a slot

curl -X POST http://localhost:8787/v1/venues/venue_sss_qc/bookings \
  -H "Authorization: Bearer at_..." \
  -H "Idempotency-Key: 8f14e45f-ea6b-4f31-9d4e-1b0f9c2a7e33" \
  -H "Content-Type: application/json" \
  -d '{
    "service_id": "svc_loan",
    "slot_start": "2026-09-18T09:30:00+08:00",
    "customer_ref": "CRM-88213",
    "priority_claimed": false
  }'
Idempotency-Key is required

Not optional, not best practice. Without it, your HTTP client's automatic retry on a timeout books the same customer twice and the venue loses a slot it cannot sell. Send a UUID per logical booking. Retrying with the same key returns the original booking for 24 hours; reusing it with a different body is a 409.

Late arrival

Every booking carries grace_minutes. Arrive later than that past the slot and the reservation is released. The customer is not turned away. They join their lane at their actual arrival position, behind people already waiting. Say this in your own interface; a customer who thinks a booking is a guaranteed time will be angry at a counter your software sent them to.

Checking a ticket

position moves. Walk-ins arrive, priority runs on separate capacity, and someone ahead may be sent home for a missing document. Render it as an estimate. "About 4 ahead of you" survives contact with reality; "You are 4th" generates complaints the venue has to answer.

Webhooks

If you are polling, you have missed the point. Webhooks are unlimited and free; the queue endpoint is cached and rate limited.

POST https://your-app.example/eqms
X-EQMS-Event: ticket.completed
X-EQMS-Delivery: evt_01J9Z8X7QK
X-EQMS-Signature: t=1758103482,v1=5257a869e7ec...

Verify every delivery

HMAC-SHA256 over the exact string {timestamp}.{raw body} using your endpoint's signing secret. Compare with a constant-time function. Reject anything whose timestamp is more than five minutes old.

const [, ts, sig] = req.headers['x-eqms-signature']
  .match(/t=(\d+),v1=([0-9a-f]+)/);

const expected = crypto.createHmac('sha256', SIGNING_SECRET)
  .update(`${ts}.${rawBody}`).digest('hex');

if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return 401;
if (Math.abs(Date.now()/1000 - ts) > 300) return 401;  // replay
Deliveries repeat

Delivery is at-least-once. Use id as an idempotency key and discard duplicates. They are normal, not a fault. Acknowledge with any 2xx within five seconds and do your real work afterwards; a slow endpoint gets retried and you will see the same event again.

Events

EventWhen
booking.createdA slot is reserved
booking.cancelledCancelled by anyone
booking.expiredGrace window passed without arrival
ticket.issuedA number was taken
ticket.calledCalled to a window. Carries the window.
ticket.servingService started
ticket.completedFinished successfully
ticket.incomplete The visit failed. Carries reason, usually a missing document.
ticket.referredSent to another service or branch
ticket.abandonedLeft without being served

ticket.incomplete is the one to build on. It tells you which of your own pre-visit instructions is failing, which is something you can fix and the venue cannot.

Tolerate event types you do not recognise and fields you did not expect. New ones are added within v1 without warning, and an integration that throws on an unknown type will break on a Tuesday for no reason you can see.

What the API will not do

Some things are missing on purpose. These are not permissions you can negotiate into a contract, and the endpoints return 501 rather than 404 so you know it is a decision rather than a gap.

Not availableWhy
GONECall the next customer Calling is a physical act. A clerk looks up, sees the previous person has actually left the window, and presses Call. Your server cannot know that. Remote calling announces numbers to an empty counter.
GONEReorder the queue Position is derived from an append-only event log under rules the venue set. That is what makes it defensible to a regulator. An external system moving people breaks the audit trail of a client you do not control.
GONEGrant priority status You may declare a claim with priority_claimed. It is verified against physical ID at the counter, exactly as a walk-in's claim is. If software could grant it, the lane that exists for senior citizens and people with disability becomes the fast lane for whoever integrates best.
GONERead staff performance A raw per-person score is misleading without case-mix adjustment, and you have no context to interpret one. Venue and service aggregates are available; people are not.

If your product needs one of these, the answer is not a bigger contract. Talk to us about what you are actually trying to achieve, because there is usually a way to get it that does not involve reaching into someone else's queue.

Personal data

Send us as little as possible. customer_ref is your identifier, a string that means something in your system. Not a name, not an address.

Never send a PhilSys Number

There is no field for it and a 12-digit value in customer_ref is rejected with 422. Under the Data Privacy Act the venue is the personal information controller for its queue data. The less that crosses this boundary, the smaller everyone's exposure and the shorter your customer's security review.

A phone number is accepted only in notify.msisdn, only where the venue has enabled notifications, and only for that booking's alerts.

Errors

RFC 9457 problem details, application/problem+json. Branch on type, never on title, which may be reworded.

{
  "type": "https://developers.e-qms.io/errors/slot-unavailable",
  "title": "That slot filled",
  "status": 409,
  "detail": "Call availability again and offer the customer another time.",
  "request_id": "req_4f2a91c4b7d3"
}
StatusMeans
400Malformed, or a required header is missing
401Token missing, expired or revoked. Re-authorise.
403Wrong venue, missing scope, or the venue disabled it
409Slot filled, idempotency key reused, already cancelled
422Well-formed but not actionable
429Rate limited. Read Retry-After.
501Deliberately not offered. See above.

Quote request_id in any support request. It is how we find your call in our logs.

Rate limits

Per app, per venue. Every response carries a RateLimit header.

WhatLimit
GET /queue120 per minute, cached 5s
Other reads600 per minute
Writes120 per minute
Webhook deliveriesUnlimited, and free

The asymmetry is the point. Our cost has to scale with the number of venues, not with how often you ask. Forty venues polled every second is 3.4 million requests a day for data that changed a few hundred times, and a business model that does not survive it.

Versioning

The version is in the path. Inside /v1 we only add: new optional fields, new endpoints, new event types. Your client must tolerate all three.

A breaking change means /v2, and /v1 keeps working for twelve months with a deprecation header on every response for the last six. If you have shipped to forty clinics you cannot migrate in a fortnight, and we would rather you integrated once than twice.

Talk to us

developers@e-qms.io. If you are building something the API does not quite support, say so before you work around it. The workaround usually becomes the thing that breaks.