API reference

djstrich API

Booking platform API for DJs, promoters, and booking agencies.

Version 1.0.0 · Base URL https://djstrich.de · 8 operations

Getting started

  1. As an owner of a promoter organisation, open your promoter profile and find API access. Create a key — it is shown once and only its hash is stored, so copy it immediately.
  2. Send it as Authorization: Bearer djs_live_… on any /api/v1/** request. The key determines which organisation you are acting for; there is no organisation parameter anywhere in v1.
  3. Discover ids: GET /api/v1/events, then GET /api/v1/events/{eventId}/slots for slot ids, and GET /api/v1/djs for DJ profile ids.
  4. Offer a slot to a DJ, or invite one who is not on djstrich yet. Then either register a webhook or poll GET /api/v1/bookings?updated_since=….
curl -X POST https://djstrich.de/api/v1/slots/$SLOT_ID/book \
  -H "Authorization: Bearer $DJSTRICH_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"dj_profile_id":"...","message":"Friday, 02:00, main room. 90 minutes."}'

Two ways to put a DJ on a slot

The DJ already has a djstrich profilePOST /api/v1/slots/{slotId}/book. This creates a booking request and notifies them. It does not assign them: the slot stays open until they accept. That is intentional — a promoter cannot put a DJ's name on a public line-up without their consent, through the API any more than through the app.

The DJ is not on djstrichPOST /api/v1/djs/invite. This returns a claimable link and tags your slots with their name. It does not create a profile; the profile exists once they open the link and sign up, at which point their slots attach automatically and an invite.accepted webhook fires.

Authentication

apiKeyAuth

http

Promoter API key, sent as `Authorization: Bearer djs_live_…`. Org-scoped and created by an organisation owner under Promoter → profile → API access. Only a SHA-256 hash is stored, so the token is shown exactly once at creation. Valid on `/api/v1/**` only; it is not accepted anywhere else on the site.

Errors

Every /api/v1/** error returns {"error": {"code", "message"}}. Branch on code, which is stable; message is human-readable English and may be reworded. Responses are never translated — a machine API should not change its payload based on who is calling it.

A resource that belongs to another organisation returns 404, not 403. The two are deliberately indistinguishable so the API never confirms that another promoter's records exist.

Outbound webhooks

Register an endpoint with POST /api/v1/webhooks (or in the dashboard). We POST a signed JSON envelope to it whenever a booking changes. Deliveries are queued and retried with backoff (1 min, 5 min, 30 min, 2 h, 6 h; five attempts). Expect delivery within about five minutes of the event, not instantly.

Exactly one event fires per transition — the most specific one that applies. booking.status_changed is the catch-all for transitions with no more specific type, not a duplicate of booking.confirmed. Subscribing to both does not double up.

Respond 2xx quickly; anything else counts as a failure, and redirects are never followed. An endpoint that fails ten times in a row is disabled until you re-enable it from the dashboard.

Verify every delivery. The X-DJStrich-Signature header is t=<unix seconds>,v1=<hex>, an HMAC-SHA256 over `{t}.{raw body}` keyed with your endpoint secret. Verify against the raw body bytes, before parsing.

// Node — verify X-DJStrich-Signature: t=<unix seconds>,v1=<hex>
import { createHmac, timingSafeEqual } from 'node:crypto'

export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(',').map((kv) => {
      const i = kv.indexOf('=')
      return [kv.slice(0, i).trim(), kv.slice(i + 1).trim()]
    })
  )
  // Reject replays: the timestamp must be within 5 minutes of now.
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false
  const expected = createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex')
  const a = Buffer.from(expected)
  const b = Buffer.from(parts.v1 ?? '')
  return a.length === b.length && timingSafeEqual(a, b)
}

Envelope

id
string
Unique per event. Shared across endpoints, so two receivers can dedupe.
type
string (booking.confirmed | booking.declined | booking.cancelled | booking.status_changed | booking.requested | invite.accepted | event.lineup_complete | event.is_beginning | event.has_ended)
created_at
string
data
object

v1

Promoter API v1 — the stable, versioned surface for driving a promoter organisation from your own systems. Authenticated with an API key; every request is scoped to the organisation that key belongs to.

GET /api/v1/bookings List booking requests (polling feed)

Every booking request of your organisation, most recently updated first. This is the polling alternative to webhooks: keep the highest `updated_at` you have seen and pass it as `updated_since` on the next call, then follow `next_cursor` until it is null. Pagination is keyset-based on `(updated_at, id)`, so rows cannot be skipped or repeated when data changes between pages.

Auth: apiKeyAuth

Parameters

  • updated_since · query ISO 8601 timestamp; only rows updated at or after this
  • status · query Filter to one booking status (e.g. confirmed, cancelled)
  • limit · query Page size, default 50, max 100
  • cursor · query The `next_cursor` from the previous response

Responses

  • 200 A page of booking requests
  • 400 Invalid cursor or updated_since
  • 401 Missing, invalid, revoked or expired API key. The body is the v1 error envelope, whose `code` is one of missing_api_key, invalid_api_key, revoked_api_key, expired_api_key.
  • 429 Too many requests. Retry after the number of seconds in `Retry-After`.
GET /api/v1/djs Search DJ profiles

Search the public DJ catalogue by name, city, country, genre and availability date. Returns approved, publicly listed profiles only. Use the `id` of a result as `dj_profile_id` when booking a slot.

Auth: apiKeyAuth

Parameters

  • q · query Free-text artist name search
  • city · query Home-base city, or "worldwide"
  • country · query ISO country code (DE, AT, CH, GB, NL, TH)
  • genres[] · query Repeatable genre filter
  • date · query Availability date (YYYY-MM-DD). Excludes DJs who have blocked that day.
  • sort · query
  • limit · query Page size, default 24, max 48
  • cursor · query Pagination cursor from the previous response

Responses

  • 200 Matching DJ profiles
  • 401 Missing, invalid, revoked or expired API key. The body is the v1 error envelope, whose `code` is one of missing_api_key, invalid_api_key, revoked_api_key, expired_api_key.
  • 429 Too many requests. Retry after the number of seconds in `Retry-After`.
POST /api/v1/djs/invite Invite a DJ who is not on djstrich yet

Creates a claimable invite link and tags the given slots with the DJ's name. This does NOT create a DJ profile. The profile comes into existence when the invitee opens the link and signs up, at which point the slots they were tagged on are attached to their new profile automatically and an `invite.accepted` webhook fires. Until then the slots show the name as an external tag. All slots must belong to your organisation; otherwise the call returns 404.

Auth: apiKeyAuth

Request body (required)

  • name · string · required The DJ's name as it should appear on the line-up
  • slot_ids · array · required 1–50 slot ids from GET /api/v1/events/{eventId}/slots
  • locale · string (de | en) Language of the invite landing page. Defaults to English.
{
  "name": "Nadia Kraft",
  "slot_ids": [
    "3c1e0b9a-7c1e-4f0a-9c1e-0b9a7c1e4f0a"
  ],
  "locale": "de"
}

Responses

  • 201 Invite created
  • 401 Missing, invalid, revoked or expired API key. The body is the v1 error envelope, whose `code` is one of missing_api_key, invalid_api_key, revoked_api_key, expired_api_key.
  • 404 The resource does not exist, or belongs to another organisation. These are deliberately indistinguishable — the API never confirms that another promoter's records exist.
  • 422 Missing name, or slot_ids empty / longer than 50
  • 429 Too many requests. Retry after the number of seconds in `Retry-After`.
GET /api/v1/events List your organisation's events

Every event belonging to the organisation the API key was issued for, newest first. There is no organisation parameter — the key determines the scope, so a key can never read another promoter's events.

Auth: apiKeyAuth

Responses

  • 200 Events for the key's organisation
  • 401 Missing, invalid, revoked or expired API key. The body is the v1 error envelope, whose `code` is one of missing_api_key, invalid_api_key, revoked_api_key, expired_api_key.
  • 429 Too many requests. Retry after the number of seconds in `Retry-After`.
GET /api/v1/events/{eventId}/slots List an event's line-up slots

The set slots of one event, including whether each is already filled and whether it is currently an open call. This is how you discover the `slot_id` values that `POST /api/v1/slots/{slotId}/book` and `POST /api/v1/djs/invite` take. An event belonging to a different organisation returns 404, not 403 — the API does not confirm that another promoter's records exist.

Auth: apiKeyAuth

Parameters

  • eventId · path · required

Responses

  • 200 Event summary plus its slots
  • 401 Missing, invalid, revoked or expired API key. The body is the v1 error envelope, whose `code` is one of missing_api_key, invalid_api_key, revoked_api_key, expired_api_key.
  • 404 The resource does not exist, or belongs to another organisation. These are deliberately indistinguishable — the API never confirms that another promoter's records exist.
  • 429 Too many requests. Retry after the number of seconds in `Retry-After`.
POST /api/v1/events/{eventId}/slots Create line-up slots on an event

Adds 1–50 new set slots to an event. Use this when an event was created without a line-up, or to extend one — `create_event` does not generate slots by itself. The batch is all or nothing: every slot is validated before anything is written, so a single bad `event_floor_id` rejects the whole call and leaves the event untouched. Slots come back in the same shape `GET` returns, so the response can be used directly to pick a `slot_id` for `POST /api/v1/slots/{slotId}/book`. An event belonging to a different organisation returns 404, not 403.

Auth: apiKeyAuth

Parameters

  • eventId · path · required

Request body (required)

  • slots · array · required 1–50 slots to create
{
  "slots": [
    {
      "slot_label": "Opening",
      "start_time": "23:00",
      "end_time": "01:00",
      "set_length_minutes": 120,
      "genres": [
        "Techno"
      ]
    },
    {
      "slot_label": "Headline",
      "start_time": "01:00",
      "end_time": "03:00",
      "set_length_minutes": 120,
      "is_open_call": true
    }
  ]
}

Responses

  • 201 The created slots
  • 401 Missing, invalid, revoked or expired API key. The body is the v1 error envelope, whose `code` is one of missing_api_key, invalid_api_key, revoked_api_key, expired_api_key.
  • 404 The resource does not exist, or belongs to another organisation. These are deliberately indistinguishable — the API never confirms that another promoter's records exist.
  • 409 The event is archived
  • 422 slots empty / longer than 50, or an event_floor_id from another event
  • 429 Too many requests. Retry after the number of seconds in `Retry-After`.
POST /api/v1/slots/{slotId}/book Offer a slot to a DJ who is on djstrich

Creates a booking REQUEST for the given slot and notifies the DJ. It does not assign them: the slot stays open until the DJ accepts, and only their acceptance fills it. This is the same contract the web app has, and it is deliberate — a promoter cannot put a DJ's name on a public line-up without their consent. The event and organisation are derived server-side from `slotId`; any values you send for them are ignored. A slot belonging to another organisation returns 404. For a DJ who does not have a profile yet, use `POST /api/v1/djs/invite` instead. Rate limit: booking creation is additionally capped per user per 24 hours (the same cap the web app applies). Exceeding it returns 429 with `retryAfterSeconds`.

Auth: apiKeyAuth

Parameters

  • slotId · path · required

Request body (required)

  • dj_profile_id · string · required From GET /api/v1/djs
  • message · string · required Message to the DJ, 1–500 characters
  • set_time · string | null HH:MM
  • set_length_minutes · integer | null
  • budget_min · number | null
  • budget_max · number | null
{
  "dj_profile_id": "8f14e45f-ceea-467a-9c8b-1f5c9c9c1a11",
  "message": "Freitag, 2 Uhr Slot im Hauptraum. 90 Minuten, Techno.",
  "set_time": "02:00",
  "set_length_minutes": 90
}

Responses

  • 201 Booking request created; the DJ has been notified
  • 401 Missing, invalid, revoked or expired API key. The body is the v1 error envelope, whose `code` is one of missing_api_key, invalid_api_key, revoked_api_key, expired_api_key.
  • 404 The resource does not exist, or belongs to another organisation. These are deliberately indistinguishable — the API never confirms that another promoter's records exist.
  • 409 The slot already holds a confirmed booking
  • 422 Validation error (missing dj_profile_id or message, message length)
  • 429 Too many requests. Retry after the number of seconds in `Retry-After`.
GET /api/v1/webhooks List your webhook endpoints

Read-only. Registering and deleting endpoints is dashboard-only, on purpose: every API key is minted by an org owner, so a leaked key could otherwise register an attacker-controlled endpoint that keeps receiving your bookings even after the key is revoked. Keeping writes behind a browser session means revoking a key actually stops the leak.

Auth: apiKeyAuth

Responses

  • 200 Registered endpoints and the available event types
  • 401 Missing, invalid, revoked or expired API key. The body is the v1 error envelope, whose `code` is one of missing_api_key, invalid_api_key, revoked_api_key, expired_api_key.
  • 403 The key was not issued by an organisation owner