Build a Ticket Integration

Scaffold an adapter, implement its provider client and canonical mappings, and add synchronization and fulfillment without bypassing platform safety.

On this page

Start disabled and let the scaffold update packaging

Create a built-in adapter and its literal SAM loader
npm run scaffold:integration -- \
  --key acme-tickets \
  --provider-name tickets.acme.example \
  --display-name "Acme Tickets"

The command validates the key and provider name, createscomponents/integrations/providers/acme-tickets-adapter.js, and inserts a sorted literal loader into components/integrations/providers/index.js. It refuses to overwrite an adapter and rolls the file back if it cannot update the loader atomically. The source template iscomponents/integrations/templates/provider-adapter.js, outside the auto-discovered provider directory.

  1. Describe only implemented behavior

    Choose a stable key and provider name, then declare the real auth methods, capabilities, actions, and disabled-by-default catalog state.

  2. Create an injectable provider client

    Put upstream requests, pagination, timeouts, rate-limit handling, and response parsing behind a client that tests can mock.

  3. Implement required hooks

    Add account, group, event, credential-refresh, configuration, disconnect, or fulfillment hooks only when their capability or action requires them. Ticket inventory is canonical data embedded in listEvents items, not a separate adapter hook.

  4. Test before enabling

    Run focused adapter tests and npm run test:integrations, then follow the migration and staged rollout guide.

Bound every upstream operation

  • Set an explicit request timeout and a response-size limit for every upstream call.
  • Own complete pagination inside the adapter; a first page is never a complete account snapshot.
  • Use bounded retries and backoff only when the provider operation is safe to repeat.
  • Forward context.signal through account, group, event, inventory, refresh, and pagination requests so a scheduled worker can stop before its Lambda timeout.
  • Translate provider failures into stable codes and safe messages; keep raw credentials and unredacted responses out of errors and logs.
  • Treat an authenticated 401 as a credential-wide reconnect condition. Treat a 403 as resource-scoped only when the provider contract explicitly guarantees that meaning.

Validate credentials against the provider before returning them from an authentication hook. Store only the access and refresh material required for later calls; do not persist the password, one-time code, cookie jar, or complete login response.

Map stable, provider-scoped identities

Minimum canonical integration data
ResourceRequired behaviorImportant details
AccountgetAccount returns a stable string externalId.Username, email, names, and image are optional. Every connection must resolve one account.
GrouplistGroups returns stable string IDs and names.Use a deterministic synthetic group derived from the upstream account when the provider has no organization concept.
EventReturn a stable string external ID and non-empty title.Map dates, venue, URL, image, status, description, and raw safe metadata when available.
TicketReturn a stable ID and finite non-negative upstream base price. Include a name and canonical ISO currency, and include availability or closed state only when the provider reports it authoritatively.Never coerce an unknown amount, inventory count, or sale state into zero or false, and never mix currencies inside one event.
Ticket inventory is embedded in each listEvents item
const mapTicket = (ticket) => {
  const rawPrice = ticket.price;
  const price = (typeof rawPrice === 'number' || typeof rawPrice === 'string')
    && String(rawPrice).trim() !== ''
    ? Number(rawPrice)
    : Number.NaN;
  if (ticket.id == null || String(ticket.id).trim() === '' || !Number.isFinite(price) || price < 0) {
    throw new Error('Provider returned invalid ticket identity or base price');
  }

  const mapped = { id: String(ticket.id), name: ticket.name, price, currency };
  if (ticket.quantityAvailable != null && ticket.quantityAvailable !== '') {
    const quantityAvailable = Number(ticket.quantityAvailable);
    if (!Number.isSafeInteger(quantityAvailable) || quantityAvailable < 0) {
      throw new Error('Provider returned invalid ticket availability');
    }
    mapped.quantityAvailable = quantityAvailable;
  }
  if (ticket.closed !== undefined) {
    if (typeof ticket.closed !== 'boolean') {
      throw new Error('Provider returned invalid ticket sale state');
    }
    mapped.closed = ticket.closed;
  }
  return mapped;
};

return {
  items: [{
    externalId: String(upstreamEvent.id),
    title: upstreamEvent.name,
    rawData: { currency },
    tickets: upstreamTickets.map(mapTicket),
  }],
  errors: [],
  totalFetched: 1,
};

Use the shared canonical-currency resolver. Put the resolved code on the event and every ticket. The platform, not the adapter, validates and snapshotsProvider.config.checkout_fee while preserving the adapter's upstream base amount, and it overwrites every Diem ownership or Provider ID.

Make complete and partial snapshots explicit

A collection hook returns an array or an envelope with items, optional safe errors, and truthful totalFetched. A malformed envelope fails the sync; it is never treated as an authoritative empty response. Report an individual enrichment failure in errors and throw only when the entire operation cannot continue.

Snapshot reconciliation behavior
ResultPlatform behavior
Complete group or event collection with no errorsSave returned items, then retain but deactivate imported items missing from that provider-account scope.
Partial collection with one or more safe errorsSave valid items, report HTTP 207/partial counts, and preserve cached groups, events, and ticket inventory that the provider could not authoritatively describe.
Thrown or malformed collectionFail the affected scope without reconciling cached data away.
Later complete snapshot returns a prior itemRestore the retained imported record and its current availability.

Perform one idempotent upstream fulfillment

Declare ticket_fulfillment only when the adapter implementsfulfillOrder. It receives the resolved credential, Provider, account, event, buyer, normalized positive-quantity items, and the platform's stable idempotency key. Issue one logical provider order and return an explicit fulfilled or failed result with a safe provider reference for reconciliation.

Fulfillment retry-safety declarations
manifest.fulfillment.retrySafetyWhen to use itStale or ambiguous dispatch
manual_reconciliationDefault when the provider does not guarantee idempotent ticket issuance under Diem's supplied key.Never dispatch automatically again; move the line to reconciliation_required.
provider_idempotencyOnly when every upstream issuance forwards the supplied idempotency key and the provider guarantees replay safety.The platform may reclaim a stale lease and retry with the same key.
Exact fulfilled and provider-confirmed failure results
// The only successful result
return {
  status: 'fulfilled',
  success: true,
  providerReference: String(upstreamOrder.id),
  raw: upstreamOrder.safeMetadata,
};

// Use only when the provider explicitly confirms failure
return {
  status: 'failed',
  success: false,
  error: {
    code: 'UPSTREAM_REJECTED',
    safeMessage: 'The ticket provider rejected this delivery.',
    retryable: false,
  },
};

Before checkout persistence and again before adapter dispatch, the platform enforces at most 10 ticket types, 10 tickets of any one type, and 20 tickets total in one order. Provider inventory and per-buyer rules may lower those ceilings; an adapter cannot raise them.

  • Forward the supplied idempotency key when the provider supports one; never mint a new key to escape an in-progress result.
  • Do not create local Diem tickets or invoke another provider path from the adapter.
  • Return retryable only for a provider-confirmed failure that is safe to repeat.
  • When a provider has no idempotency primitive, use a deterministic reference or query-before-create reconciliation strategy where possible.
  • Return a provider reference support can compare directly with the upstream order or guest list.

Results fail closed. A missing return, empty object, unknown status, or contradictory status/success pair raises INVALID_FULFILLMENT_RESULT and never becomes a delivered ticket. A provider-idempotent adapter may retry that ambiguous result under the same key; every other adapter enters reconciliation.

Next guide

Integration Contract
Build a Ticket Integration | Diem Developer Documentation