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
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.
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.
Create an injectable provider client
Put upstream requests, pagination, timeouts, rate-limit handling, and response parsing behind a client that tests can mock.
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.
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.signalthrough 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
| Resource | Required behavior | Important details |
|---|---|---|
| Account | getAccount returns a stable string externalId. | Username, email, names, and image are optional. Every connection must resolve one account. |
| Group | listGroups returns stable string IDs and names. | Use a deterministic synthetic group derived from the upstream account when the provider has no organization concept. |
| Event | Return a stable string external ID and non-empty title. | Map dates, venue, URL, image, status, description, and raw safe metadata when available. |
| Ticket | Return 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. |
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.
| Result | Platform behavior |
|---|---|
| Complete group or event collection with no errors | Save returned items, then retain but deactivate imported items missing from that provider-account scope. |
| Partial collection with one or more safe errors | Save 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 collection | Fail the affected scope without reconciling cached data away. |
| Later complete snapshot returns a prior item | Restore 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.
| manifest.fulfillment.retrySafety | When to use it | Stale or ambiguous dispatch |
|---|---|---|
manual_reconciliation | Default 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_idempotency | Only 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. |
// 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.