Integrating with CowTicket

Your website owns the event pages and the brand. CowTicket runs availability, orders, payment and check-in behind this API. This page is the whole integration — four endpoints cover a normal storefront.

Authentication

Create an API key in the app (API Keys page — the raw key is shown once). Your server exchanges it for a short-lived JWT, then sends that as a Bearer token. Do both from your backend — never ship the API key or the JWT to the browser.

POST https://api.cow-ticket.dev/accounts/token
api-key: <your-api-key>

{ "accountId": "<your-account-id>", "type": "customer" }

→ 200 { "token": "<jwt>", "exp": "..." }   // valid 1 day — cache and refresh

# All calls below:
authorization: Bearer <jwt>

1 · Show what's on sale

Returns ACTIVE ticket types currently inside their selling window, with a live available count, plus the event's refund policy so you can show it before checkout. Prices are in satang (฿1 = 100 satang).

GET /events/ticketConfigs?eventCode=my-event-2026

→ 200 {
  "data": [{
    "code": "early-bird", "name": "Early Bird",
    "group": "Day 1 (Sat 14 Mar)",   // section label, or null
    "price": 90000, "currency": "thb",
    "available": 42, "limitPerOrder": 4,
    "startSellingDate": "...", "endSellingDate": "..."
  }],
  "refundPolicy": { "allowed": true, "termEn": "...", "termTh": "..." }
}

data comes back in the organizer's chosen display order — keep it. group is an optional section label organizers use to split ticket options into sessions or days: start a new titled section whenever it changes, and render null-group items flat with no heading. It's display text only — no shared behavior, and one order can mix items from different groups.

2 · Create an order

Reserves the tickets atomically (no overselling) and returns a Stripe Checkout URL — redirect the buyer there. Unpaid orders expire after 1 hour and release their seats. Free orders skip payment: status comes back PAID with checkoutUrl: null. metadata is yours — attendee form answers put here appear in the organizer's exports.

POST /orders

{
  "accountId": "<your-account-id>",
  "eventCode": "my-event-2026",
  "email": "buyer@example.com",
  "items": [{ "code": "early-bird", "quantity": 2 }],
  "metadata": { "phone": "081-234-5678" },
  "successUrl": "https://yourevent.com/thanks?orderId={orderId}",
  "cancelUrl": "https://yourevent.com/tickets"
}

→ 200 {
  "orderId": "...", "orderNo": "...",
  "status": "PENDING", "expiresAt": "...",
  "checkoutUrl": "https://checkout.stripe.com/..."
}

3 · Confirm payment

When the buyer lands on your successUrl, poll the order until it's PAID (webhooks usually beat the redirect, but don't assume). Statuses: PENDING → PAID → REFUNDED, or EXPIRED/CANCELED.

GET /orders/detail?orderId=...

→ 200 { "status": "PAID", "tickets": [...], "refundPolicy": {...}, ... }

4 · Show the tickets

The order's confirmation email links back to your successUrl — your page renders the tickets and their QR codes (encode the ticket id). At the event, staff scan with the organizer's scanner setup; a ticket redeems once, and voided or refunded tickets are rejected automatically.

GET /tickets/getIdsByOrder?orderId=...   → ticket ids
GET /tickets/detail?ticketId=...         → holder, status, type

# Scanner side (organizer JWT):
POST /tickets/redeem  { "ticketId": "...", "gate": "main" }

Good to know

  • Currency is Thai Baht; every price and amount is satang.
  • Payments are direct charges on the organizer's connected Stripe account — connect Stripe in the app before selling paid tickets.
  • Ticket issuance is idempotent — a webhook retry can't double-issue tickets.
  • The buyer gets one email from us: the order confirmation with a receipt and a link to your successUrl. Everything else the buyer sees is your site.
  • Stuck or missing something? support@cow-ticket.dev — we read everything.