Skip to main content
Create checkouts on your server. The customer pays on Skinloop; your server confirms the result before delivering the order.

Integration steps

Never expose the API key in browser code. Return URLs are navigation, not proof of payment. Keep CS2 orders reserved during trade protection.

Prerequisites

In the merchant dashboard:
  • register the HTTPS origins used by any per-request success and cancellation URLs (as in this example), or save both complete URLs as checkout defaults and omit them from the request,
  • create an API key with checkout:create and checkout:read, and
  • configure a public HTTPS webhook endpoint and securely store its whsec_... signing secret.
Store the API URL copied from the dashboard and both secrets in your server environment:
Use the complete signature receiver for the webhook route and Safe fulfillment for the fulfillment table, enqueue operation, and worker.

Complete integration example

The example below includes order storage, checkout creation, status recovery and fulfillment. Expand it when you are ready to implement the full flow.

1. Persist the order before creating a checkout

Create your local order first. Store integer USD cents, a monotonically increasing attempt number, and the exact idempotency key used for that attempt:
Persisting the key first makes recovery safe. If your process stops after Skinloop creates the checkout but before your update commits, retry with the same request and key. Skinloop returns the original checkout instead of creating another one.

2. Create and persist the hosted checkout

This server function reads the expected order, calls the real hosted-checkout endpoint, validates the returned identity, and stores the checkout ID before it returns the redirect URL:
201 means Skinloop created the checkout. 200 means the same idempotent request recovered an existing checkout. Handle both identically.Expose this through an authenticated route in your own application. The browser calls your route; it never calls Skinloop with the merchant key.

3. Redirect the customer

Redirect to the exact hostedUrl returned by your server. Do not construct or modify it:
The success and cancellation URLs are navigation only. Show a “confirming payment” state there while your server waits for a signed webhook or reads the checkout status.

4. Confirm authoritative completion

Use the signed webhook receiver as the primary path. After authenticating the event, look up the local order with data.merchantOrderId, verify data.requiredAmount and data.currency exactly against the stored order, and call the same recordCompletionAndEnqueue operation shown below. data.amount is the actual accepted skin value and can be greater than data.requiredAmount; require it to be at least the required amount and verify data.overpaymentAmount equals the difference. Convert all three decimal strings to integer cents without binary floating point.Use status polling as recovery when a webhook is delayed. The real status endpoint returns the same hosted-checkout projection:
The three state helpers above can be implemented as guarded updates. Requiring the stored checkout ID prevents a result from one attempt from changing another attempt:
Poll with bounded exponential backoff and jitter, or use a scheduled recovery job. Do not run an unbounded request loop in the customer’s browser. Webhooks and polling can observe completion concurrently, so both must call the same enqueue operation.

5. Schedule duplicate-safe fulfillment

recordCompletionAndEnqueue must update the order and insert one durable job in the same local database transaction. The complete implementation is in Safe fulfillment. Its unique checkout_id, merchant_order_id, and idempotency_key constraints make a duplicate webhook or simultaneous status poll return the existing job.The fulfillment worker runs separately after the handler commits. It sends the same stable destination idempotency key on every attempt:
Never call the external fulfillment destination from the webhook request, status poller, success page, or checkout-creation route.

6. Handle every non-success outcome

For a customer retry after a safe terminal result, increment checkout_attempt, persist a new idempotency key, and then create a new checkout. Never replace a pending, active, hold, completed, or reconciliation-required checkout.If the fulfillment destination times out after it may have delivered the order, do not create another checkout or blindly deliver again. Query the destination with the same idempotency key. If its result cannot be determined, set the fulfillment job to needs_reconciliation and require manual review.

End-to-end result

The integration is complete when:
  • refreshing or retrying checkout creation recovers the same Skinloop checkout,
  • the customer is redirected only to the returned hostedUrl,
  • redirects never mark an order paid,
  • only an authenticated completed result with fulfillmentAllowed: true enqueues fulfillment,
  • webhook delivery and polling converge on one fulfillment job,
  • safe terminal failures release the reservation, and
  • ambiguous payment or delivery outcomes stop for reconciliation instead of risking a duplicate order.
Next: run this flow with a small test checkout, including a duplicate create request and a duplicate webhook, before following the test-to-live checklist.