Skip to main content
Anything that moves money is safe to retry only if the server can recognise the retry as a duplicate. SkinShark gives you two mechanisms, chosen per endpoint:
  • Idempotency-Key header on /fund — transient retry safety with a short TTL.
  • externalId body 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.
Generate the key once per logical operation, not per HTTP attempt. If your checkout retries on network errors, all retries should send the same key.

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 with idempotent: true (no duplicate created).
  • Same externalId, different email/steamId → 409 EXTERNAL_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.
Or with raw fetch:
Use 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

Pick externalId 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 quote endpoints — quotes are short-lived (TTL embedded in expiresIn); if you retry within the TTL you’ll get a different quote. Don’t retry on network failure; create a fresh quote.
  • Deposit create / session endpoints — 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:
If your quote expires mid-flow:

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.