Tap redirects send endstate_chip_id
Additive, nothing removed. A tap redirect appends the tapped chip’s id as endstate_chip_id, next to the one-time credential endstate_e, so a destination you own has everything it needs to verify the tap:@endstate-sdk/core reads a redirect destination for you:Retry a create safely with Idempotency-Key
Every call that creates something now accepts an Idempotency-Key header, so a create whose response you never saw can be retried without creating the resource twice.POST /v1/units, POST /v1/collections, POST /v1/chips, and POST /v1/units/{unit_id}/chip-replacements join POST /v1/taps, POST /v1/chips/bulk, and the claim and transfer calls, which already accepted one.- Send the same key on the retry and you get the original response back with
Idempotent-Replayed: true. - The header is optional. Omit it and nothing about these calls changes.
- Updates (
PATCH,PUT) still need no key: they set fields to the values you send, so a repeat leaves the same result.
Breaking: the tap endpoints moved to /v1/taps
Verifying a tap, simulating one, and listing tap history are now three operations on one resource.Migration: send
chip_id in the request body rather than in the path - the response is otherwise unchanged. For tap history, the list key is taps instead of scans; pagination is unchanged. Clients generated from openapi.json get createTap, listTaps, and createTestTap.Verify a tap, with or without a backend
POST /v1/taps records a tap and returns a session token. One endpoint takes either credential: your server calls it with an API key, or your page calls it directly with a publishable key and no server of your own behind it.- Send
chip_idand the one-timeevalue from the tap URL - plus the optionalcif the tag provides one. The response is the tapid, the session token with its expiry and what it is bound to, the chip, the unit, and the redirect URL. - Authenticate with
end_sk_...from your server, orend_pk_live_...from the browser. This is the only endpoint that accepts a publishable key. ttlanddry_runare API-key only - a server is trusted to choose a token lifetime; a browser is not. A publishable-key call gets the 600-second default.- Calling from the browser also needs the origins your page is served from registered with
PUT /v1/settings/cors-origins, or the browser will block the call.
Simulate a tap without hardware
POST /v1/test-helpers/taps produces a valid single-use e for a test chip - the same value a physical chip emits when it is tapped. Send chip_id; the response is { "chip_id": ..., "e": ..., "count": ... }. Feed chip_id and e straight to POST /v1/taps to exercise verification end to end. Secret key only, and test chips only.See Testing without hardware.Added: fetch a single chip
GET /v1/chips/{chip_id} returns one chip by its identifier, with a snapshot of
the unit it is paired to (or null when the chip is not paired yet). Same chip
fields as GET /v1/chips, plus the full unit and its issuance status - so you
no longer have to page the list to read one chip.Added: stable operationId on every endpoint
Every operation in the OpenAPI spec now carries a hand-chosen operationId
such as createTap, createUnit, and listTaps. Clients generated from
openapi.json get these as method names instead of names derived from the URL
path. These names are stable and will not be renamed in place.Added: staging base URL in the OpenAPI spec
servers now lists staging alongside production. See
Environments.Added: optional c verification code on pairing and tap endpoints
POST /v1/chips (pairing) and POST /v1/taps (verify) now accept
an optional c - the 16-character hex verification code some tap URLs include
alongside e. Nothing changes for existing integrations; include c whenever
you have it to prepare for upcoming chip-verification hardening. See
the c value.Breaking: one canonical session-token shape
GET /v1/session-tokens/current now returns the same shape as the session_token object in the tap response (minus token, which is only shown once at issuance): the binding moved into a nested scope object.Before: { "chip_id": ..., "unit_id": ..., "organization_id": ..., "expires_at": ... }
After: { "expires_at": ..., "scope": { "chip_id": ..., "unit_id": ..., "organization_id": ... } }Migration: read the three identifiers from scope.* instead of the top level. expires_at is unchanged.Browser calls: use the default credentials mode
Responses no longer sendAccess-Control-Allow-Credentials. Endstate authenticates with the Authorization header, which is not a browser “credential”, so the header was never needed - but if your client-side code passes credentials: "include", the browser will now block the response with an opaque CORS error.Migration: remove credentials: "include" from fetch calls to the API. The default mode ("same-origin") sends no credentials on a cross-origin request, which is what the API expects. Keep sending Authorization: Bearer ... exactly as before - it is unaffected by the credentials mode. Server-side calls are unaffected.Browser responses now also expose X-Request-Id, the X-RateLimit-* headers, Retry-After, and Idempotent-Replayed, so client code can read them off both successes and errors.Publishable keys
Retrieve the publishable key that identifies your organization:GET /v1/publishable-keysreturns your active keys. If you have none, one is created automatically, so a first call always returns a usable key.- A publishable key (
end_pk_live_...) is safe to embed in browser code: it identifies your organization and grants no access by itself. Retrieving it requires your secret API key.
Manage your own CORS origins
Calling the API from a browser no longer requires asking Endstate to allowlist your origins:GET /v1/settings/cors-originsreturns the origins allowed to call the API from a browser;PUTreplaces the list (send an empty array to remove all origins).- Each entry is an exact origin (
https://brand.example), a subdomain wildcard (https://*.brand.example), or a port wildcard (http://localhost:*) - one wildcard per entry. Entries are stored in canonical form and returned as saved. - Changes take effect within about a minute. Remove development origins before you go live.
Tap history
See every verified tap of your chips, not just the per-chip counter:GET /v1/tapsreturns a cursor-paginated history of verified taps. Filter bychip_id,unit_id, or a time window; test-chip taps are excluded unlessis_test=true.- Each item’s
idmatches theidPOST /v1/tapsreturned when the tap was recorded.
Pair chips in bulk
Pair up to 100 chips in a single request - built for encoding lines and batch fulfillment:POST /v1/chips/bulkaccepts a list of the same per-chip payloads asPOST /v1/chips(test or encoded). All units must belong to the same collection, and units are issued in the order provided.- The request is all-or-nothing: if any entry fails validation, nothing is paired.
- Send an
Idempotency-Keyheader to retry safely. - New error codes:
chip.bulk_mixed_collections,chip.bulk_pending.
Transfer a unit between owners
A unit can now move from its current owner to a new owner - tap-authorized like a claim, but executed by the current owner:POST /v1/units/{unit_id}/transfers(session token required) creates the transfer and returns atransactionpayload. Only the unit’s current owner can submit it - Endstate authorizes the transfer but never relays it, and the authorization is valid for 30 minutes.GET /v1/units/{unit_id}/transfers/{transfer_id}(API key or session token) polls status:prepared→confirmed;expiredandfailedare terminal.- New error codes:
transfer.in_progress,transfer.already_to_recipient,transfer.owner_unknown,transfer.not_found.
Replace a unit’s chip
Until a unit is claimed, its chip can now be replaced - for example when a chip is damaged or fails QA after pairing but before the item reaches its owner:POST /v1/units/{unit_id}/chip-replacementspairs an existing, unpaired chip to the unit and retires the current one. Endstate performs the swap; replacement is asynchronous.GET /v1/units/{unit_id}/chip-replacements/{replacement_id}polls status (prepared→submitted→confirmed;failedis terminal).- New error codes:
chip_replacement.locked(unit already claimed - the chip is permanently locked),chip_replacement.in_progress, andchip_replacement.not_found.
chain_id is now optional on collection create
Collections are created on your organization’s default network when chain_id is omitted - configured by Endstate at onboarding, so most integrations never need to send a network identifier. An explicit chain_id is still accepted as an override, but must be one of the networks enabled for your organization.GET /v1/settings now returns your network configuration alongside default_redirect_url: default_chain_id (the default network, or null if none is configured) and allowed_chain_ids (the networks an explicit chain_id may use). Both are read-only.No migration needed - requests that send chain_id keep working unchanged.Single-resource responses are now top-level (breaking)
POST, GET, and PATCH responses for collections and units previously wrapped the resource in an envelope - { "collection": {...} } / { "unit": {...} }. They now return the resource’s fields at the top level, making every single-resource response uniform (chips, taps, session tokens, claims, and settings were already top-level).Migration: if your integration reads response.collection.id or response.unit.id from these endpoints, read the same fields at the top level instead (response.id, response.collection.token, …). List responses are unchanged - they still wrap under a plural key alongside pagination. See Response shape.Empty request bodies accepted on all-optional POSTs
APOST with a JSON content-type and an empty body previously failed JSON parsing. An empty or whitespace-only body is now treated as {}, so a POST whose body fields are all optional works without a body. Endpoints with required body fields still validate as before.Tap redirects
Control where a user lands after tapping a chip, at three levels - the most specific one wins:redirect_urlis a new (nullable, always-present) field on the unit and collection resources, accepted on create and editable via newPATCH /v1/units/{unit_id}andPATCH /v1/collections/{collection_id}endpoints.- New
GET/PATCH /v1/settingsendpoints manage the org-widedefault_redirect_url. - Redirected taps carry the tapped chip’s id and its one-time credential as query parameters, so your page can verify the tap.
Claims
Transfer on-chain ownership of a unit to a recipient wallet, authorized by the session token from a recent tap of that unit’s chip:POST /v1/units/{unit_id}/claimscreates a claim (endstate_relayorclient_broadcastexecution).GET /v1/units/{unit_id}/claims/{claim_id}polls status (claiming→claimed/expired/failed).

