API and integrations

Your point of sale, ERP or online shop creates tickets, changes their state and gets every step by webhook. Clients get the same notifications as from the app.

On request during launch: write to us and we switch the API on for your shop.

BASEhttps://api.tfa9adni.com/v1

Get started

Every request goes to this address, over HTTPS, with JSON both ways.

Every answer has the same envelope: success and data, or success false and an error with a code. Amounts are in minor units (millimes, cents) with the currency and its decimals. Dates are ISO 8601, in UTC.

GET/v1/shop

Test the connection

Returns the shop behind the key, its currency, its time zone and the key’s scopes.

Create a key in the app: Shop › Integrations. Only the shop owner can. Then test the connection:

Authentication

Send the key in the Authorization header. A key only reaches its own shop.

The key is shown once, when it is created. Keep it on your server or in your till software, never in a web page or a message. A revoked key stops working within a minute. Five active keys per shop at most.

What a key can do

  • tickets:readRead tickets.
  • tickets:writeCreate tickets and change their state.
  • clients:readFind a client and read their loyalty card.
  • loyalty:writeAdd stamps and use a reward.

“Full access” gives all four, “Read only” gives tickets:read and clients:read.

Tickets

Ticket states

  • workshopIn the workshop, being worked on.
  • readyReady, waiting for the client.
  • impossibleCannot be fixed, the item waits for its client.
  • collectedCollected. outcome says whether it was ready or not possible.
  • abandonedClosed: never collected, or no news from the shop for 60 days. abandon_reason says which (uncollected, no_news).

POST/v1/tickets

Create a ticket

Creates a ticket in the workshop. Its number follows the app’s.

The Idempotency-Key header is required: if the network drops, send the same request with the same key and you get the same ticket with replayed true, never a duplicate. Numbers follow the app’s.

Headers

  • Idempotency-Key string required

    One unique key per attempt: the same request sent again returns the same ticket.

JSON body

  • service string required

    The service, as in the app.

  • type string optional

    repair or prepare (order). repair by default.

  • item string optional

    The item dropped off.

  • description string optional

    A note for the workshop.

  • items array optional

    Several items (2 to 20): label, service, price_minor.

  • client_id uuid optional

    A client who already knows the shop.

  • client_phone string optional

    The client’s number: the ticket is offered to them.

  • price_minor integer optional

    The price, in minor units.

  • deposit_minor integer optional

    The deposit, in minor units.

  • promised_at datetime optional

    The promised day, ISO 8601.

  • external_ref string optional

    Your own reference, 64 characters at most.

client_phone links nobody: the ticket is offered to the account holding that number, which accepts it or blocks the shop. The answer is the same whether an account exists or not.

GET/v1/tickets

List tickets

Filter, page through and sync the shop’s tickets.

Filters: state (several, comma separated), number, client_id, phone, external_ref, created_after, created_before, updated_since. limit from 1 to 100 (50 by default). When has_more is true, send next_cursor back as cursor. With updated_since the order follows the update time: handy for syncing.

Query parameters

  • state string optional

    One or more states, comma separated.

  • number integer optional

    The ticket number.

  • phone string optional

    The number typed on the ticket.

  • external_ref string optional

    Your reference.

  • updated_since datetime optional

    Only what changed since this date.

  • limit integer optional

    1 to 100, 50 by default.

  • cursor string optional

    The next_cursor of the previous page.

GET/v1/tickets/{id}

Read a ticket

One ticket by id. Another shop’s ticket answers 404.

GET/v1/tickets/by-number/{n}

Find by number

The most recent open ticket with this number.

Numbers start again at 1 after 9999: by-number returns the newest open ticket, else the newest, and meta.others lists the rest.

POST/v1/tickets/{id}/status

Change the state

Ready, impossible, back to the workshop or collected.

The same rules as in the app: ready or not possible from the workshop, collected from ready or not possible, back to the workshop from ready or not possible. A collected ticket can be reopened for 24 h, after that the answer is 409 with reason REOPEN_WINDOW. The client gets the same notification, and loyalty counts as in the app.

JSON body

  • status string required

    ready, impossible, workshop or collected.

POST/v1/tickets/{id}/notified

Client notified

You told the client yourself.

You told the client yourself, by SMS or on the phone? Say so, and their page shows it.

JSON body

  • channel string required

    sms, whatsapp or call.

Clients and loyalty

A shop only sees clients who already know it: a ticket accepted there or a visit to the counter. Looking up an unknown number, or the number of a client who blocked the shop, gives the same 404.

The name follows the client’s choice: if they hide it, you see “Sana B.”.

GET/v1/clients

Find a client

By phone, among clients who already know the shop.

Query parameters

  • phone string required

    The number, in international format.

GET/v1/clients/{id}

Read a client

Display name, open and total tickets, loyalty card.

GET/v1/clients/{id}/loyalty

Loyalty card

The shop’s programme and the client’s card.

POST/v1/clients/{id}/loyalty/stamps

Add stamps

Adds 1 to 10 stamps to the card.

Stamps and rewards need an Idempotency-Key. At most 20 stamps per client, per day, per key. When loyalty is off the answer is 409 LOYALTY_OFF.

Headers

  • Idempotency-Key string required

    Required.

JSON body

  • count integer required

    1 to 10.

POST/v1/clients/{id}/loyalty/redeem

Use a reward

Uses one reward available on the card.

Headers

  • Idempotency-Key string required

    Required.

Webhooks

Give an https address in the app: Tfa9adni sends it a POST at every step of a ticket. One webhook per shop.

  • ticket.createdA ticket is created, in the app or through the API.
  • ticket.readyThe ticket is ready.
  • ticket.impossibleThe ticket is marked not possible.
  • ticket.reopenedThe ticket goes back to the workshop.
  • client.notifiedThe client was told (WhatsApp, app, SMS, call).
  • ticket.collectedThe client picked up their item.
  • ticket.abandonedThe ticket closed: ready and never collected (60 days), or left in the workshop without news (60 days). reason is uncollected or no_news.
  • quote.acceptedThe client accepts a new price.
  • client.linkedA client is linked to the ticket.
  • pingThe app’s “Send a test” button.

Check the signature

Every delivery carries the Tfa9adni-Signature header: t is the send time in seconds, v1 the HMAC-SHA256 of “t.body” with the webhook secret, in hex. Compute it on the raw body, compare in constant time, and refuse a t more than 5 minutes off. For 24 h after a secret change, two v1 are sent.

Good practice

  • Answer 2xx within 10 seconds, then do the work later.
  • An event can arrive twice: deduplicate on id.
  • Ignore types you don’t know: new ones will come.
  • To fill a gap, read GET /v1/tickets?updated_since= again.

Without a 2xx answer, it tries again after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h, then gives up. After 20 failures in a row over 72 h, or a 410 answer, the webhook turns off and the owner is told. Redirects are not followed and private addresses are refused.

Errors

On an error, success is false and error.code says why. details.reason explains a 409.

400 VALIDATION_ERROR Invalid body or parameter, details says which.
401 UNAUTHENTICATED Missing, wrong or revoked key.
403 FORBIDDEN The key lacks this right (details.scope).
403 SHOP_NOT_APPROVED The shop is not approved yet.
403 ACCOUNT_DEACTIVATED The shop is deactivated.
403 API_DISABLED The API is not open for this shop yet.
404 NOT_FOUND Not found for this shop.
409 CONFLICT Not possible in this state (details.reason).
422 PROFANITY The text holds a forbidden word.
429 TOO_MANY_ATTEMPTS Too many requests or tickets, try again later.

Limits

  • 120 per minuteRequests per key
  • 30 per minuteWrites per key
  • 150 per hour, 600 per dayTickets per shop (app and API together)
  • 60 per hour, 300 per dayClient lookups by phone

Every answer carries the RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers. Past the limit the answer is 429: wait RateLimit-Reset seconds.