Idempotency-Keyheader on/fund— transient retry safety with a short TTL.externalIdbody field on sub-user creation,/market/buy, and/market/buy/quick— your own permanent correlation id, doubles as the natural retry key.
Idempotency-Key header (on funding)
POST /merchant/users/{id}/fund requires an Idempotency-Key header.
Replays with the same key — for the same merchant — return the original
transaction with idempotent: true.
With @skinshark/sdk
The SDK auto-generates an Idempotency-Key (UUIDv4) for every fund call
when you don’t pass one. To pin retries to a key derived from your own
checkout id, pass it explicitly:
With raw fetch
Scope
Idempotency keys are scoped per merchant:merchant:<merchantId>:<key>.
Two different merchants can use the same key without colliding, and a
sub-user’s externalId never affects scope.
Errors
externalId (on sub-user creation, buys, quick-buys)
POST /merchant/users, POST /market/buy, and POST /market/buy/quick
accept an externalId field. The server treats it as the natural retry
key.
Sub-user creation
createSubUser replays on matching externalId:
- Same
externalId, same email/steamId → returns the existing user withidempotent: true(no duplicate created). - Same
externalId, different email/steamId → 409EXTERNAL_ID_TAKEN.
Buys
market.buy and market.quickBuy replay on matching externalId —
calling either with the same externalId for the same sub-user returns
the existing Trade rather than creating a new one. This makes the
buy + retry pattern idempotent without any extra header.
externalId as your join key everywhere — when you reconcile
SkinShark trades back into your own order table, you look them up by your
own checkout ID, not the SkinShark UUID.
Picking values
PickexternalId values that uniquely identify a logical operation —
don’t recycle them across different intents. A retried checkout reuses
the same ID; a fresh checkout gets a fresh ID.
Operations without idempotency
Some endpoints don’t have idempotency support — duplicates from network failures need different handling:- Catalog reads (search, listings, item detail) — GETs, free to retry.
- Deposit
quoteendpoints — quotes are short-lived (TTL embedded inexpiresIn); if you retry within the TTL you’ll get a different quote. Don’t retry on network failure; create a fresh quote. - Deposit
create/sessionendpoints — retrying after a network failure can produce two pending deposits. Pattern: walk recent deposits for the sub-user and resume the existing one rather than creating fresh:
When to retry vs reconcile
The
@skinshark/sdk HTTP layer auto-retries 408/429/5xx with exponential
backoff and Retry-After-honouring delays. POST/PATCH retries are gated
on the presence of idempotencyKey to prevent double-charges.