Developers

Quick Start

The shortest path from credentials to a first reward issued.

Written against
The v1 specification
Runnable
Locally — against the in-repo sandbox server
Environment
Sandbox
Endpoints used
Eight

Overview

One loop, end to end

Every integration reduces to the same loop: describe a customer, give them a wallet, define what earns value, send the events your business already produces, and read the balance that results. This page walks that loop once, in the sandbox environment, with nothing left out.

The goal is a working end-to-end path in a test environment before any decision about assets, custody or production rollout. Nothing on this page touches production, and nothing here issues anything on a public network.

Each step is the exact call the specification defines — the same paths, headers and error codes that the Examples page uses at greater length.

Requirements

What you need before the first call

  • An approved access request. There is no public signup — access is reviewed, and an organization is created when it is granted.
  • A sandbox API key. Keys are created in the dashboard, are scoped to one organization and one environment, and the full secret appears exactly once, in the creation response. After that, only the key prefix is ever shown — store the secret when you see it.
  • Your organization id, sent as X-Organization-ID on every authenticated call. It is validated against your memberships on every request; a mismatch is rejected as organization_mismatch.
  • Node 20 or later, or any HTTP client. The snippets below use bare fetch — no SDK is required for this loop.

Installation

There is nothing to install

No package is published yet, so the setup is one helper around fetch. This is the same helper the Examples page builds; it adds the required headers and unwraps the response envelope.

matinee.tstypescript
const BASE = 'https://sandbox-api.matinee.ai/v1'

export async function call<T>(
  path: string,
  options: { idempotencyKey?: string; body?: unknown; method?: string } = {},
): Promise<T> {
  const response = await fetch(BASE + path, {
    method: options.method ?? (options.body ? 'POST' : 'GET'),
    headers: {
      Authorization: `Bearer ${process.env.MATINEE_API_KEY}`,
      'X-Organization-ID': process.env.MATINEE_ORG_ID!,
      'X-Environment': 'sandbox',
      'X-Request-ID': crypto.randomUUID(),
      'Content-Type': 'application/json',
      ...(options.idempotencyKey ? { 'Idempotency-Key': options.idempotencyKey } : {}),
    },
    body: options.body ? JSON.stringify(options.body) : undefined,
  })

  const payload = await response.json()
  if (!response.ok) throw new Error(`${payload.error.code} (${payload.meta.request_id})`)
  return payload.data as T
}

The headers are §4 of the specification; the { data, meta } envelope is §5. Success and failure both carry meta.request_id — log it, it is what support will ask for.

The path

Six steps to a balance that moved

  1. Customer01

    Create the customer record, keyed by your own external id.

  2. Wallet02

    Give the customer somewhere to hold what they earn.

  3. Asset03

    Create the points asset and activate it — nothing can be issued against an inactive asset.

  4. Rule04

    Create a programme and add one rule to its draft version, then publish.

  5. Event05

    Send one business event, the same shape your systems already produce.

  6. Balance06

    Read the wallet balance and see the reward the rule issued.

Examples

The whole loop in one file

Idempotency keys on every write mean the whole file can be re-run safely: a repeated call returns the original result instead of creating a duplicate.

quick-start.tstypescript
// 1–2. A customer, and somewhere to hold what they earn.
const customer = await call('/customers', {
  idempotencyKey: 'quick-start-customer',
  body: { external_id: 'user-0001', email: 'ada@example.com', country: 'GB' },
})
const wallet = await call('/wallets', {
  idempotencyKey: `wallet-${customer.id}`,
  body: { customer_id: customer.id, type: 'customer' },
})

// 3. The asset being earned. Created configured, then activated.
const points = await call('/assets', {
  idempotencyKey: 'quick-start-points',
  body: { name: 'Points', symbol: 'PTS', type: 'points', decimals: 0 },
})
await call(`/assets/${points.id}/activate`, { idempotencyKey: `activate-${points.id}` })

// 4. A programme with one rule: 10 points per completed signup. Rules attach
//    to a draft version and take effect when the version is published.
const programme = await call('/reward-programs', {
  idempotencyKey: 'quick-start-programme',
  body: { name: 'Getting started', asset_id: points.id },
})
await call('/reward-rules', {
  idempotencyKey: 'quick-start-rule',
  body: {
    program_id: programme.id,
    event_type: 'signup.completed',
    actions: [{ type: 'issue', asset_id: points.id, amount: { fixed: '10' } }],
  },
})
await call(`/reward-programs/${programme.id}/publish`, {
  idempotencyKey: `publish-${programme.id}-v1`,
})

// 5. The event. From here on, this is the only call your system makes.
await call('/events', {
  idempotencyKey: 'signup-user-0001',
  body: {
    type: 'signup.completed',
    external_event_id: 'signup-user-0001',
    customer_id: customer.id,
    occurred_at: new Date().toISOString(),
  },
})

// 6. The balance. Rewards are issued asynchronously, so poll or subscribe to
//    the reward.issued webhook rather than asserting on the very next read.
const balances = await call(`/wallets/${wallet.id}/balances`)

Events are deduplicated on organization, environment and external_event_id — sending the same event id twice returns the original result rather than issuing twice.

Run it

This page runs today, locally

The repository ships a sandbox reference server that implements this exact surface — the headers, the envelope, idempotency, rate limits, the ledger and the reward engine — in memory, on localhost. The hosted sandbox does not exist yet; this does.

terminalshell
$ git clone https://github.com/matinee-ai/matinee.ai && cd matinee.ai
$ npm ci
$ npm run sandbox
# Prints a base URL, an API key and an organization id, ready to paste
# into the helper above. State is in memory and dies with the process.

The integration tests run every snippet on this page against this server through the reference SDK — the loop above is executed, not aspirational.

Playground

Or touch the engine right here

The reward engine, the double-entry ledger and the store behind the loop above are dependency-free TypeScript — so this page runs them. Publish a rule, complete an order, send the same order twice, simulate, redeem. Every number is derived from real ledger entries living in this browser tab; nothing calls an API.

The rule

v1 published

The events

Balance0 PTS
Redeemed0 PTS
Ledger net0 ✓

Recent ledger transactions

Nothing posted yet. Complete an order.

This playground runs the sandbox reference server’s actual store, ledger and engine modules, compiled into this page — the same code the integration tests drive over HTTP. Every balance above is derived from real double-entry entries, which is why the ledger always nets to zero. It calls no API, stores nothing, and resets when you leave.

Best practices

Habits worth forming on day one

  • Send an Idempotency-Key on every write. Reusing a key with a different body is rejected as idempotency_key_reused rather than quietly applying the second request — derive keys from your own stable identifiers.
  • Log meta.request_id from every response, success or failure. It is echoed from X-Request-ID and written to the request log, so it is the one string that connects your logs to ours.
  • Simulate a rule before publishing it. POST /reward-rules/{id}/simulate evaluates against a sample event and writes nothing — no ledger transaction, no execution row.
  • Stay in sandbox until the loop works. A key cannot cross environments: pointing a sandbox key at production fails as environment_mismatch, never as a silent switch.
  • Send the events your systems already emit rather than inventing new ones. The engine deduplicates on your external event id, so retrying a delivery is always safe.

API

The surface this page touches

Eight endpoints, each gated by a permission your key must hold. Every response carries the same { data, meta } envelope.

POST /customers

Create the customer record, keyed by your external id.

customers.create

POST /wallets

Create a wallet for a customer.

wallets.create

POST /assets · POST /assets/{id}/activate

Create the asset, then activate it. Activation is a transition with preconditions, not a flag.

assets.create · assets.edit

POST /reward-programs · POST /reward-rules

Create the programme and add a rule to its draft version.

rewards.create · rewards.edit

POST /reward-programs/{id}/publish

Publish the draft. The published version becomes immutable.

rewards.activate

POST /events

Submit a business event. The highest-volume write on the whole surface.

events.write

GET /wallets/{id}/balances

Read what the wallet holds, served from a snapshot over the ledger.

wallets.view

Error handling

The failures you will meet first

Every error carries a stable code, a type that maps to the HTTP status, and the request id. On this path, expect these:

  • validation_failed — the request body failed validation. The details field says which fields; fix and resend.
  • asset_not_active — you skipped the activation step, or issued against a paused asset. Activate first.
  • reward_program_not_active — the rule exists but the version holding it was never published.
  • event_duplicate — an event with this external_event_id already exists. Expected under retries, not exceptional: the original result is returned.
  • rate_limit_exceeded — back off and retry. The only code here where retrying unchanged is the right response.
  • organization_mismatch and environment_mismatch — the key does not belong to the organization or environment you named. A configuration error, never a transient one.

The full registry — 84 codes, each mapped to a type and status — is on the API Reference page.

Ready to run this for real?

Access is reviewed. Tell us what you are building and we will let you know when the sandbox opens.