AskWhiz
Developers · API v1

Ticket intake API

Connect your call centre, CRM or ERP to AskWhiz. Send who needs service, which equipment and what is wrong; AskWhiz turns it into a request your dispatchers accept on WhatsApp or on the web.

Endpoint
POST https://api.askwhiz.io/api/v1/tickets
Authentication
Organization API key (Bearer)
Format
JSON, snake_case, always 202 on success
Scope
Create only · service companies only

What it does

Your call centre, CRM or ERP sends AskWhiz the client, the equipment and the problem. AskWhiz turns it into a submitted request for your company, exactly like a request a restaurant sends from the app: your dispatchers are notified and accept it, on WhatsApp or on the web, by choosing the technician and the visit time.

  • Only service companies can use the API: organizations with the service-provider role in AskWhiz. Requests always go to your own dispatchers.
  • The API only creates requests. It does not list, read, change or cancel them; your team does that in AskWhiz.
  • There is no field for a technician, an appointment or your own reference number: your dispatchers choose the technician and the time when they accept the request.

How clients and equipment are matched

AskWhiz never guesses. Each request uses the records it finds with certainty, creates the ones that do not exist yet, or asks a dispatcher to choose.

Clients

AskWhiz looks for the client among your company's clients without an AskWhiz account and the restaurants linked to your company in AskWhiz:

  • the same phone number or email with a fitting name (the same name, or one containing the other) is a certain match; a linked restaurant is matched by its company phone and its name;
  • the same name is a certain match too: capital letters, accents, punctuation and endings such as SRL, S.A. or GmbH are ignored;
  • the same phone or email under another name (one owner with several restaurants, a manager's phone) is never used on its own: a dispatcher decides;
  • a name that only contains the other (“Restaurant Centru” and “Restaurant Centru Cluj”), without the same phone or email, is only a possible match.

Exactly one certain match, and no same phone or email under another name: that client is used. A client without an AskWhiz account also gains any detail it lacks (address, phone, email, contact) from your request; nothing is overwritten. No match: AskWhiz adds the client to your client list. Otherwise (several certain matches, the same phone or email under another name, or only possible matches) a dispatcher chooses, and can also say the client is new.

Equipment

Each unit is then matched on its own among that client's registered equipment, in this order:

  1. the same serial number (unless both brands are known and differ);
  2. then the same brand and model (the same model when you send no brand);
  3. then the same type, for example “Combi oven”.

The first step that finds anything decides: one unit is used; several units are left to a dispatcher; nothing at any step adds a new unit to the client. A registered unit whose serial, brand or model contradicts yours is never matched by a weaker key, and two units of one request never silently become the same machine: a dispatcher is asked.

Several units in one request

Send every unit of one call-out in one request: 1 to 20 units.

  • A client without an AskWhiz account gets one ticket with every unit.
  • A restaurant linked to you in AskWhiz gets as many tickets as the units-per-ticket limit of your service agreement with it requires. If it allows one unit per ticket, three units become three tickets; tickets lists them all.

What awaiting_dispatcher means

The client or a unit matched more than one record, so AskWhiz did not guess. Your dispatchers get a numbered WhatsApp message with the choices, and the request also waits at the top of the requests inbox in AskWhiz on the web. A dispatcher picks the right records, or says a record is new, and the ticket is created then.

If nobody chooses within 2 working hours, the dispatchers are reminded once; 2 working hours later your admins get an email. Your system does not need to do anything. The API does not report the ticket created later: replaying the same Idempotency-Key still answers awaiting_dispatcher. Keep request_id in case you contact AskWhiz support.

Get an API key

An organization admin creates keys in the AskWhiz web app under Settings → API keys (the tab is shown only to admins of service companies). Name each key after the system that will use it, and optionally let it expire in 30 days, 90 days or 1 year.

The key looks like awz_live_… and is shown once, right after it is created. AskWhiz stores only a hash of it, so a lost key cannot be shown again: revoke it and create a new one. An organization can hold up to 20 active keys.

Keep the key on your server, in a secret store or an environment variable. Never put it in a web page, a mobile app or a code repository.

When a key stops working

  • an admin revokes it (immediately, answering 401 API_KEY_REVOKED);
  • its expiry date passes (401 API_KEY_EXPIRED);
  • the admin who created it leaves the organization or is no longer an admin (401 API_KEY_OWNER_INACTIVE): ask a current admin for a new key;
  • your organization is suspended (403 ORG_SUSPENDED): the key works again once an admin restores billing.

A key acts for your organization with the authority of the admin who created it. Every ticket it creates is marked “via API” in the ticket history, with the key's name visible only inside your organization.

Send a request

One endpoint. The body is JSON with snake_case field names. Call it from your server only.

HTTP
POST https://api.askwhiz.io/api/v1/tickets

Headers

HeaderRequiredValue
AuthorizationYesBearer followed by your full API key.
Content-TypeYesapplication/json
Idempotency-KeyYesA value you choose, unique per request, for example your call or case number: 1 to 255 visible ASCII characters. See below.
Headers
Authorization: Bearer awz_live_abcdefghijk2_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
Idempotency-Key: call-2026-09-28-0042

Idempotency-Key: retries are safe

Every call needs an Idempotency-Key header: 1 to 255 visible ASCII characters (letters, digits and punctuation; no spaces, no accents). Use one value per real request, for example your call or case number.

  • Sending the same key again with the same body never creates a second client, unit or ticket. It returns the original answer, still 202, with the header Idempotent-Replayed: true. A request that first waited for a dispatcher is still answered awaiting_dispatcher.
  • Reusing a key with a different body fails with 409 IDEMPOTENCY_KEY_REUSED.
  • A call that was answered with an error created nothing and did not use up its key: fix the request and send it again.
  • Idempotency keys belong to one API key, so two of your systems cannot collide.

So when a call times out, or you get a 429 or a 5xx, send it again with the same key and the same body.

Body fields

FieldTypeRequiredLimitNotes
clientobjectYes—Who needs the service.
client.namestringYesup to 300 charactersCompany or site name. Used to find the client.
client.phonestringNew client *up to 40 charactersInternational format, e.g. +40744123456. Used to find the client; a client without an AskWhiz account gets the visit details on WhatsApp at this number.
client.emailstringNew client *up to 320 charactersUsed to find the client.
client.addressstringNoup to 500 charactersThe site address. Gives the technician a route and lets AskWhiz confirm the arrival on site.
client.contact_namestringNoup to 300 charactersThe person on site.
equipmentarrayYes1 to 20 unitsThe units that need service. Each is matched on its own.
equipment[].typestringAt least one †up to 100 charactersWhat it is, e.g. “Combi oven”.
equipment[].brandstringAt least one †up to 200 characterse.g. “Rational”.
equipment[].modelstringAt least one †up to 200 characterse.g. “iCombi Pro 6-1/1”.
equipment[].serialstringAt least one †up to 200 charactersThe most reliable way to find a registered unit.
problemstringYesup to 8000 charactersWhat is wrong, in the caller's words. Line breaks are kept.
prioritystringNolow, normal, high, urgentDefault normal.

* When the client is new to your company, send a valid phone number or email (or both); otherwise the call fails with 422 CUSTOMER_CONTACT_REQUIRED.

† Each unit needs at least one of type, brand, model or serial.

Every value is a string: send phone numbers in quotes too. Spaces around a value are trimmed and repeated spaces collapsed, and a blank value counts as missing. Control characters (other than line breaks and tabs) and any field not listed here are refused with 400 VALIDATION_ERROR.

Phone numbers: international format

Send the country code with +, for example +40744123456 or +40 744 123 456 (spaces and dashes are fine). A national number such as 0744123456 is understood only when your organization has a country set in AskWhiz; otherwise it is refused with 422 CLIENT_PHONE_INVALID.

Responses

An accepted request always answers 202 Accepted with three fields:

FieldMeaning
request_idIdentifies this API request. Quote it if you contact AskWhiz support.
statusticket_created or awaiting_dispatcher.
ticketsThe tickets created. Empty while waiting for a dispatcher; usually one; several for a linked restaurant whose agreement limits the units per ticket.
tickets[].idThe ticket's id.
tickets[].numberThe ticket number your team sees in AskWhiz, e.g. TKT-000042.
tickets[].statusAlways submitted: waiting for a dispatcher to accept it.

status: ticket_created

The ticket exists and your dispatchers were notified.

202 Accepted
{
  "request_id": "0f6d3c1e-5a53-4f4c-9d0a-5e2b1c9e7a10",
  "status": "ticket_created",
  "tickets": [
    {
      "id": "7b1e2f9a-3c4d-4e5f-8a6b-9c0d1e2f3a4b",
      "number": "TKT-000042",
      "status": "submitted"
    }
  ]
}

Several units, several tickets

The three units of the “Several units” example, for a linked restaurant whose service agreement allows one unit per ticket:

202 Accepted
{
  "request_id": "5d2a8c4e-1b7f-4e3a-9c6d-2f8e0a1b3c5d",
  "status": "ticket_created",
  "tickets": [
    {"id": "c0a8e5d1-2b3c-4d5e-8f6a-7b8c9d0e1f2a", "number": "TKT-000043", "status": "submitted"},
    {"id": "d1b9f6e2-3c4d-4e5f-9a7b-8c9d0e1f2a3b", "number": "TKT-000044", "status": "submitted"},
    {"id": "e2c0a7f3-4d5e-4f6a-8b8c-9d0e1f2a3b4c", "number": "TKT-000045", "status": "submitted"}
  ]
}

status: awaiting_dispatcher

A dispatcher must first choose the client or a unit (see What awaiting_dispatcher means above). tickets is empty; the ticket is created when a dispatcher chooses.

202 Accepted
{
  "request_id": "9a4f1c2e-7d3b-4a5e-8f0c-1b2d3e4f5a6b",
  "status": "awaiting_dispatcher",
  "tickets": []
}

The Idempotent-Replayed header

A repeated Idempotency-Key with the same body is answered with the original body and the header Idempotent-Replayed: true. A first answer does not carry the header.

Errors

Every error has the same JSON shape. Branch on code; message is for people and may change.

422 Unprocessable Entity
{
  "error": {
    "code": "CUSTOMER_CONTACT_REQUIRED",
    "message": "This client is new to your company: send a valid phone number or email address.",
    "details": {}
  }
}

VALIDATION_ERROR lists each problem in details.errors, with the field path:

400 Bad Request
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input",
    "details": {
      "errors": [
        {"field": "body.equipment.0", "message": "Value error, Describe the equipment: type, brand, model or serial"},
        {"field": "body.priority", "message": "Input should be 'low', 'normal', 'high' or 'urgent'"}
      ]
    }
  }
}
StatuscodeMeaningWhat to do
400VALIDATION_ERRORThe body is not valid JSON, or a field is missing, blank, too long, of the wrong type, not an allowed value, or not part of the API.Fix the fields listed in details.errors.
400IDEMPOTENCY_KEY_REQUIREDNo Idempotency-Key header.Send one, unique per request.
400IDEMPOTENCY_KEY_INVALIDThe Idempotency-Key is empty, longer than 255 characters, or contains spaces or non-ASCII characters.Use only letters, digits and punctuation.
401API_KEY_MISSINGNo Authorization header, or it does not start with Bearer.Send Authorization: Bearer awz_live_….
401API_KEY_INVALIDThe key is incomplete, mistyped or does not exist.Copy the full key again. If it was lost, ask an admin for a new one.
401API_KEY_REVOKEDAn admin revoked the key.Ask an admin for a new key.
401API_KEY_EXPIREDThe key's expiry date has passed.Ask an admin for a new key.
401API_KEY_ORG_INACTIVEThe organization that owns the key was deleted.Contact AskWhiz.
401API_KEY_OWNER_INACTIVEThe admin who created the key left the organization or is no longer an admin.Ask a current admin to create a new key.
403ORG_SUSPENDEDYour organization is suspended.An admin restores billing in AskWhiz; the same key then works again.
403LEGAL_CONSENT_REQUIREDThe admin who created the key has not accepted the current AskWhiz terms.That admin signs in to AskWhiz and accepts them.
403PHONE_VERIFICATION_REQUIREDThe admin who created the key has not verified their phone number.That admin verifies it in AskWhiz.
403SERVICE_PROVIDER_REQUIREDYour organization is not a service company.Only service companies can send requests. Contact AskWhiz.
404NOT_FOUNDThe URL is wrong, or the API is temporarily switched off.Check the URL. If it is right, retry later with the same Idempotency-Key.
409IDEMPOTENCY_KEY_REUSEDThis Idempotency-Key was already used with a different body.Use a new key for a new request. To get the original answer, resend the original body.
422CLIENT_PHONE_INVALIDclient.phone is not a valid phone number. A national number needs a country set on your organization.Send it in international format, e.g. +40744123456.
422CLIENT_EMAIL_INVALIDclient.email is not a valid email address.Correct it or leave it out.
422CUSTOMER_CONTACT_REQUIREDThe client is new to your company and the request has no valid phone or email.Send client.phone or client.email.
429RATE_LIMITEDMore than 60 requests in a minute with this key.Wait the seconds in the Retry-After header, then retry with the same Idempotency-Key.
500INTERNAL_ERRORSomething failed on the AskWhiz side.Retry with the same Idempotency-Key and a growing delay: it never creates a duplicate.

Retry only 429, 5xx and network errors, with the same Idempotency-Key and a growing delay. Any other 4xx fails the same way until the request or the account is fixed.

A 502, 503 or 504 without this JSON body comes from the network in front of AskWhiz: treat it like a 5xx.

Examples

A new client with one unit

The client is not in AskWhiz yet, so a phone number or an email is required. AskWhiz adds the client and the oven, and creates the ticket.

curl
curl -sS -X POST https://api.askwhiz.io/api/v1/tickets \
  -H "Authorization: Bearer $ASKWHIZ_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: call-2026-09-28-0042" \
  -d '{
    "client": {
      "name": "Bistro Central",
      "phone": "+40744123456",
      "email": "office@bistro-central.example",
      "address": "Str. Memorandumului 1, Cluj-Napoca",
      "contact_name": "Ion Pop"
    },
    "equipment": [
      {
        "type": "Combi oven",
        "brand": "Rational",
        "model": "iCombi Pro 6-1/1",
        "serial": "E11SI22051234"
      }
    ],
    "problem": "Shows a heating error and does not reach temperature.",
    "priority": "high"
  }'

Several units

Three units of one call-out. “Restaurant Centru” is a restaurant linked to you whose agreement allows one unit per ticket, so the answer lists three tickets (see Responses).

curl
curl -sS -X POST https://api.askwhiz.io/api/v1/tickets \
  -H "Authorization: Bearer $ASKWHIZ_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: call-2026-09-28-0043" \
  -d '{
    "client": {"name": "Restaurant Centru", "phone": "+40744111222"},
    "equipment": [
      {"brand": "Pitco", "model": "SE14", "serial": "G1234"},
      {"type": "Salamander grill"},
      {"type": "Ice machine", "brand": "Scotsman"}
    ],
    "problem": "Fryer trips the breaker; the grill and the ice machine are due for service.",
    "priority": "normal"
  }'

A retry with the same Idempotency-Key

The first call timed out and you do not know whether it arrived. Send the same body (here saved in request.json) with the same key: you get the original answer, marked Idempotent-Replayed, and no duplicate.

curl
# The first call timed out: send the SAME body with the SAME key.
curl -sS -i -X POST https://api.askwhiz.io/api/v1/tickets \
  -H "Authorization: Bearer $ASKWHIZ_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: call-2026-09-28-0042" \
  -d @request.json

HTTP/2 202
content-type: application/json
idempotent-replayed: true

{"request_id": "0f6d3c1e-5a53-4f4c-9d0a-5e2b1c9e7a10", "status": "ticket_created", "tickets": [{"id": "7b1e2f9a-3c4d-4e5f-8a6b-9c0d1e2f3a4b", "number": "TKT-000042", "status": "submitted"}]}

Python (requests)

Retries 429, 5xx and network errors with a growing delay that honours Retry-After, always with the same Idempotency-Key.

Python
import os
import time

import requests

API_URL = "https://api.askwhiz.io/api/v1/tickets"
API_KEY = os.environ["ASKWHIZ_API_KEY"]


def send_ticket(payload: dict, idempotency_key: str, attempts: int = 5) -> dict:
    """Send one request. Retries 429, 5xx and network errors with the SAME key."""
    headers = {"Authorization": f"Bearer {API_KEY}", "Idempotency-Key": idempotency_key}
    for attempt in range(attempts):
        try:
            r = requests.post(API_URL, json=payload, headers=headers, timeout=30)
        except requests.RequestException:
            r = None  # network error: the call may or may not have arrived
        if r is not None and r.status_code == 202:
            return r.json()  # {"request_id", "status", "tickets"}
        if r is not None and r.status_code != 429 and r.status_code < 500:
            error = r.json()["error"]  # fix the request; retrying it unchanged fails again
            raise RuntimeError(f"{r.status_code} {error['code']}: {error['message']}")
        if attempt == attempts - 1:
            break
        wait = 2 ** attempt  # 1, 2, 4, 8 s
        if r is not None and r.headers.get("Retry-After", "").isdigit():
            wait = max(wait, int(r.headers["Retry-After"]))
        time.sleep(wait)
    raise RuntimeError("AskWhiz is unavailable: retry later with the same Idempotency-Key")


result = send_ticket(
    {
        "client": {"name": "Bistro Central", "phone": "+40744123456"},
        "equipment": [{"type": "Combi oven", "brand": "Rational"}],
        "problem": "Does not reach temperature.",
    },
    idempotency_key="call-2026-09-28-0042",  # e.g. your call or case number
)
print(result["status"], [t["number"] for t in result["tickets"]])

JavaScript (fetch)

The same logic for Node.js 18 or newer, on your server.

JavaScript
// Node 18+ (built-in fetch). Server side only: never ship the key to a browser.
const API_URL = "https://api.askwhiz.io/api/v1/tickets";

export async function sendTicket(payload, idempotencyKey, attempts = 5) {
  for (let attempt = 0; attempt < attempts; attempt++) {
    let res = null;
    try {
      res = await fetch(API_URL, {
        method: "POST",
        headers: {
          Authorization: `Bearer ${process.env.ASKWHIZ_API_KEY}`,
          "Content-Type": "application/json",
          "Idempotency-Key": idempotencyKey,
        },
        body: JSON.stringify(payload),
      });
    } catch {
      // network error: the call may or may not have arrived; retry with the same key
    }
    if (res?.status === 202) return res.json(); // { request_id, status, tickets }
    if (res && res.status !== 429 && res.status < 500) {
      const { error } = await res.json(); // fix the request; retrying it unchanged fails again
      throw new Error(`${res.status} ${error.code}: ${error.message}`);
    }
    if (attempt === attempts - 1) break;
    const retryAfter = Number(res?.headers.get("Retry-After")) || 0;
    const wait = Math.max(retryAfter, 2 ** attempt); // 1, 2, 4, 8 s
    await new Promise((resolve) => setTimeout(resolve, wait * 1000));
  }
  throw new Error("AskWhiz is unavailable: retry later with the same Idempotency-Key");
}

const result = await sendTicket(
  {
    client: { name: "Bistro Central", phone: "+40744123456" },
    equipment: [{ type: "Combi oven", brand: "Rational" }],
    problem: "Does not reach temperature.",
  },
  "call-2026-09-28-0042", // e.g. your call or case number
);
console.log(result.status, result.tickets.map((t) => t.number));

Limits

  • 60 requests per minute per API key. Above that the answer is 429 RATE_LIMITED with a Retry-After header in seconds. Retries count too.
  • 1 to 20 units per request.
  • problem: up to 8000 characters.
  • Other text fields: see the limits in the field table.
  • Idempotency-Key: 1 to 255 visible ASCII characters.
  • Up to 20 active API keys per organization.
429 Too Many Requests
HTTP/2 429
retry-after: 17

{"error": {"code": "RATE_LIMITED", "message": "Too many requests for this API key. Retry after the indicated delay.", "details": {"retryAfterSeconds": 17}}}

What happens next

  1. 1Your dispatchers are notified of the new request, like for any request from a restaurant.
  2. 2A dispatcher accepts it, on WhatsApp or on the web, choosing the technician and the visit time.
  3. 3The client is told. A client without an AskWhiz account gets a WhatsApp message with the technician, the visit time and the address when you sent its phone number, and again if the time or the technician changes. A linked restaurant follows the job in its own AskWhiz account.
  4. 4The job runs in AskWhiz like any other: the visit, the work report and the sign-off.

The API is create-only for now: it does not send status changes back to your system. Your team follows the job in AskWhiz.

Questions? Write to contact@askwhiz.io and include the request_id.