Skip to content

Take payments on your site ​

Sell an item from your coin's store on your own website. Your server opens a checkout session, you send the buyer to its link, the buyer pays on chaindaddy.io and comes back to your site. Your server learns the payment went through from a signed webhook or by reading the session.

Rolling out

Checkout sessions are being switched on in stages. GET /api/v2/iap/config carries a checkoutSessions block where they are on. While it says "testOnly": true, only a test store's key can open a session (Test checkout on your site).

What you need ​

  • A store for your coin, and a store key (cd_iap_…) with the orders:write scope to open sessions and orders:read to read them. See Store keys. The key stays on your server. Never put it in a web page.
  • A verified site. The pages the buyer returns to must be on one of your coin's verified sites, or a subdomain of one.
  • The Seller Terms, signed by the wallet that opened the store. They already cover selling from your own site.

The fee is the same ​

A sale through a checkout session costs exactly what the same item costs on chaindaddy.io: 2.5% of the sale, or 0% when your coin launched in a Chain Daddy launchpad pool. The buyer pays the price you set, and your share goes to your payout address in the same transaction. See Fees.

1. Open a session ​

One call from your server:

bash
curl -X POST https://api.chaindaddy.io/api/v2/iap/checkout/sessions \
  -H "Authorization: Bearer $CHAINDADDY_STORE_KEY" \
  -H "Idempotency-Key: cart-8842-attempt-1" \
  -H "Content-Type: application/json" \
  -d '{
    "lineItems": [{ "sku": "credits.100", "quantity": 1 }],
    "successUrl": "https://example.com/thanks",
    "cancelUrl": "https://example.com/cart",
    "clientReferenceId": "cart-8842"
  }'
json
{
  "id": "cs_0f3a9c1e5b7d2a4c6e8f0b1d3a5c7e9f1b3d5a7c9e0f2a4c",
  "url": "https://chaindaddy.io/_pay/cs_0f3a9c1e5b7d2a4c6e8f0b1d3a5c7e9f1b3d5a7c9e0f2a4c",
  "status": "open",
  "livemode": true,
  "expiresAt": "2026-10-10T19:00:00Z"
}

The answer also repeats what you sent, so the fields below can be read back later.

Field
lineItemsExactly one item: its sku and a quantity (1 when left out). The price is the item's own
successUrlWhere the buyer returns after paying. https, on one of your coin's verified sites
cancelUrlWhere the buyer returns if they leave without paying. Same rule
clientReferenceIdOptional. Your own id for this checkout, up to 128 characters. It comes back as appAccountToken on the order, its events and its receipt. The buyer can read their own order and receipt, so use a reference, not a secret
metadataOptional. Your own JSON object, up to 20 keys and 4 KB. Returned when you read the session
walletOptional. When set, only this wallet can pay the session
expiresInSecondsOptional. How long the session can be paid: 900 (15 minutes) to 86400 (24 hours). 3600 (one hour) when left out

The buyer never sees metadata.

Always send an Idempotency-Key. Retrying the same request with the same key returns the session it opened, with status 200 and the header Idempotent-Replayed: true, so a timeout never opens two. The same key with a different request answers 422 IDEMPOTENCY_KEY_REUSED.

Only your store key can open a session. A signed-in session, your own included, answers 403 STORE_KEY_REQUIRED.

2. Send the buyer to the link ​

url is the page the buyer pays on. A plain link is enough:

html
<a href="https://chaindaddy.io/_pay/cs_0f3a9c1e…">Pay with Chain Daddy</a>

Or redirect to it from your server. The buyer signs in and confirms the payment on chaindaddy.io, never on your page, and is then sent to your successUrl (or your cancelUrl if they leave).

What the buyer sees ​

The page names your store, the item and the quantity, and opens the same purchase sheet your coin page uses. The buyer:

  1. signs in with their wallet, if they are not signed in already;
  2. sees the price, which is locked when they press Buy;
  3. confirms in their wallet;
  4. presses Done, and comes back to your site.

If they close the sheet without paying, they stay on the page, where they can pay after all or press Cancel and go back. A test store's session says "Test mode" and is completed with no payment, by a wallet that manages your coin page: sign in as yourself to try it. If you set wallet, the page says which wallet can pay.

One buyer pays a session at a time. While one wallet has a purchase under way, anyone else opening the same link is told someone else is paying (409 CHECKOUT_SESSION_IN_USE); the link frees itself when that purchase expires unpaid, in 20 minutes at most. Open one session per buyer rather than sharing a link.

After paying, the buyer's browser goes to your successUrl with the session's id added. Whatever query and fragment your URL had are kept:

text
https://example.com/thanks?cart=8842&session_id=cs_0f3a9c1e…

Leaving without paying goes to cancelUrl the same way. Use session_id to look the session up on your server; do not treat arriving there as payment.

The return is checked again at that moment. If your site has stopped being one of your coin's verified sites since you opened the session, the buyer is not sent anywhere: the page tells them how the payment ended and that they can close it. Your webhook still arrives.

The button ​

Add one script and one attribute, and the same link opens the checkout in a popup. The buyer never leaves your page:

html
<script async src="https://js.chaindaddy.io/v1/cd.js"></script>

<a href="https://chaindaddy.io/_pay/cs_0f3a9c1e…" data-cd-checkout data-cd-success="/thanks">Pay with Chain Daddy</a>
  • It is still a link. With no script, or when the browser blocks the popup, the buyer follows it and comes back to your successUrl, as above.
  • data-cd-success is the page to go to once the buyer has paid. session_id is added to it, exactly as on your successUrl. Leave it out and the page stays where it is.
  • The element fires cd:checkout when the visit ends. event.detail.status is completed, cancelled, closed (the popup closed and said nothing) or redirected (the popup was blocked and the page is going by link). On completed, event.detail.session holds the session's id, its orderId and its status.
  • Your page must be on one of your coin's verified sites, over https, for the checkout to answer it. Anywhere else the buyer still pays and is returned by link.
html
<script>
  document.addEventListener('cd:checkout', (e) => {
    if (e.detail.status === 'completed') showThanks(e.detail.session);   // for display only
  });
</script>

If your page has no session until the buyer clicks, open it from your own code. Pass a function that fetches the link: the popup opens at once, inside the click, and goes to the session when it arrives.

js
payButton.addEventListener('click', async () => {
  const result = await ChainDaddy.checkout({
    url: () => fetch('/api/checkout', { method: 'POST' }).then((r) => r.json()).then((s) => s.url),
    successUrl: '/thanks',
  });
});

The script only opens a checkout session on chaindaddy.io, keeps nothing in the browser, and treats nothing it hears as payment: confirm the payment on your server. Every option is in the JavaScript SDK reference.

In a popup, by hand ​

The button does this for you. If you would rather write it yourself, your page opens url in a popup and is told how it ended. The checkout only talks to the window that opened it, and only when that window's page is on one of your coin's verified sites.

html
<button id="pay">Pay with Chain Daddy</button>
<script>
  const CHECKOUT = 'https://chaindaddy.io';
  document.getElementById('pay').addEventListener('click', () => {
    // Open inside the click, or the browser blocks the popup.
    const popup = window.open(sessionUrl, 'cd-checkout', 'popup,width=480,height=820');
    if (!popup) { location.assign(sessionUrl); return; }   // blocked: use the link
    // Keep saying hello while the popup is open, also after it answers:
    // a checkout the buyer reloaded has to hear it again.
    const hello = setInterval(() => {
      if (popup.closed) clearInterval(hello);
      else popup.postMessage({ type: 'cd:checkout.hello' }, CHECKOUT);
    }, 1000);
    window.addEventListener('message', (e) => {
      if (e.source !== popup || e.origin !== CHECKOUT) return;   // only our popup, only chaindaddy.io
      if (e.data.type === 'cd:checkout.completed') showThanks(e.data.session);   // { id, status, orderId }
      if (e.data.type === 'cd:checkout.cancelled') showCart(e.data.session);     // { id }
    });
  });
</script>
MessageSentsession
cd:checkout.helloBy your page, repeatedly, to the popup, for as long as it is open
cd:checkout.readyBy the checkout, each time it has checked your page's origin
cd:checkout.completedBy the checkout, when the buyer paid. The popup then closesid, orderId, and status: complete when the payment is confirmed, paid when it was sent and is still confirming
cd:checkout.cancelledBy the checkout, when the buyer cancelled. The popup then closesid

Always check e.origin and e.source as above. A completed message is for your page's display only: deliver from your server, on the webhook or by reading the session. If the checkout never answers your hello (your page is not on a verified site, or the browser cut the popup off from your page), the buyer still pays and is returned by link, as above.

3. Confirm the payment ​

The buyer landing on successUrl does not prove they paid: anyone can open that address. Deliver when Chain Daddy tells your server.

By webhook. Your store's webhook gets iap.order.confirmed when the payment is confirmed. For an order paid through a session, the event's payload carries checkoutSessionId, and appAccountToken is your clientReferenceId:

json
{
  "type": "iap.order.confirmed",
  "data": {
    "orderId": "0x9e1f…",
    "payload": {
      "checkoutSessionId": "cs_0f3a9c1e…",
      "appAccountToken": "cart-8842",
      "sku": "credits.100",
      "quantity": 1,
      "status": "confirmed",
      "receipt": "eyJhbGciOiJFZERTQSIs…"
    }
  }
}

order.created and order.paid carry checkoutSessionId too. Deliver on order.confirmed.

Match the event to your checkout by checkoutSessionId, which only an order made from your session carries. Do not rely on appAccountToken alone: a buyer can set it on an ordinary order in your store.

By reading the session. With a key that has orders:read:

bash
curl https://api.chaindaddy.io/api/v2/iap/checkout/sessions/cs_0f3a9c1e… \
  -H "Authorization: Bearer $CHAINDADDY_STORE_KEY"

status is open, complete once the payment is confirmed, or expired. order is the order that paid the session (while it is open, the buyer's latest attempt), and receipt is that order's signed receipt once the session is complete. Check the receipt the same way as any other: Verifying receipts on your server.

A buyer can try more than once while a session is open. Each try is an order of its own, and the first one confirmed completes the session; no new try can start after that, or after expiresAt. A try the buyer already started can still be finished. If the same buyer pays two tries, each confirms and delivers like any order, so treat each order.confirmed as one purchase.

What a session cannot sell ​

  • An item that belongs to an app installed on your page. Those are sold through the app.
  • An unlock, an item with no price, or an archived item.
  • More than one of an item a wallet owns once (non_consumable).

Errors ​

Errors are {"error": "...", "code": "..."}.

StatuscodeWhat happened
400RETURN_URL_NOT_VERIFIEDsuccessUrl or cancelUrl is not an https page on one of your coin's verified sites. The message lists the sites that pass. Verify the site first
400INVALID_REQUESTA field is missing or out of range. The message says which
400IDEMPOTENCY_KEY_REQUIREDNo Idempotency-Key header
401INVALID_STORE_KEYThe store key is unknown or revoked
403STORE_KEY_REQUIREDCalled with a signed-in session instead of a store key
403SCOPE_REQUIREDThe key lacks orders:write (to open) or orders:read (to read)
403STORE_NOT_LIVEThe store is paused, or the plan of the wallet that opened it lapsed
403SELLER_TERMS_REQUIREDThe wallet that opened the store has not signed the Seller Terms
404SKU_NOT_FOR_SALENo such item in your store
404CHECKOUT_SESSIONS_DISABLEDCheckout sessions are not on here yet, or are open to test stores only and the key is a live store's
404NOT_FOUNDNo such session, or it belongs to another store
409SKU_NOT_FOR_SALEThe item cannot be sold through a session. See What a session cannot sell
409CHECKOUT_SESSION_IN_USEOn the buyer's order: another wallet is paying this session right now. It frees itself when that purchase expires
409CHECKOUT_SESSION_EXPIRED, CHECKOUT_SESSION_COMPLETEOn the buyer's order: the session can no longer be paid, or already was. Open a new one
403CHECKOUT_SESSION_WALLETOn the buyer's order: the session names another wallet
422IDEMPOTENCY_KEY_REUSEDThis Idempotency-Key already opened a different session. Use a new key