Commerce API reference
Commerce v1.32 covers box openings, vaulting, sell-backs, redemptions, secondary marketplace and P2P trading. This is the endpoint reference for building your own integration. To run the storefront template, use the quickstart.
The API includes app configuration, discovery, customer records, payments, openings, holdings, shipping and signed order events. Marketplace and P2P use the community endpoints. Check capabilities before enabling an action.
The API is designed for server-to-server use. Never put a DYLI API key in browser or mobile client code. Start with partner setup: register a Commerce app, keep one server key, and choose your existing authentication or optional managed login. DYLI resolves the application and slug from the key. Privy is not required for partners with their own authentication and wallet infrastructure.
What your integration needs
You provide:
- A backend that can make HTTPS requests and keep an API key secret
- A stable customer ID and an Abstract wallet address for each customer receiving an item
- A database or other durable store for your own customer, cart, idempotency, and order references
You do not need access to our database, a chain indexer, or DYLI infrastructure credentials. DYLI operates pricing, inventory claims, payment verification, order state, and fulfillment.
Choose the payment experience that fits your product:
| Payment experience | What you operate | What DYLI operates |
|---|---|---|
| Hosted Stripe Checkout | Redirect the customer to the returned URL | Stripe account, Checkout Session, payment webhook, verification, and order creation |
| Embedded Stripe Checkout | Render Stripe.js with the returned publishable key and client secret | Stripe account, PaymentIntent, payment webhook, verification, and order creation |
| Partner treasury USDC | Your customer billing or ledger, treasury wallet, and wallet signer | Payment instructions, onchain verification, replay protection, and fulfillment |
| Customer-paid USDC | Customer wallet connection and signature | Payment instructions, onchain verification, replay protection, and fulfillment |
Hosted and embedded card checkout do not require your own Stripe account or Stripe webhook. If you collect customer funds yourself and settle from a treasury wallet, that separate customer-payment system remains yours to operate.
Stripe Checkout includes a promotion-code field, including in the example app. Codes are configured in DYLI's Stripe account; ask DYLI about a promotion with the products and terms you have in mind. Stripe checks eligibility and calculates the discount; your app should still confirm the checkout through DYLI. A 100%-off checkout can complete without a PaymentIntent.
For a discounted card order, price_breakdown.total_cents is the amount actually paid. The original item and fee prices stay in the breakdown, with stripe_discount_cents and its stripe_item_discount_cents, stripe_partner_fee_discount_cents, and stripe_card_fee_discount_cents components recorded separately. Commerce earnings use sales and fees after those discounts. USDC payments still require the exact quote amount.
Partner webhooks are optional. A small integration can poll GET /orders/{order_id}; use signed webhooks when you want push-based updates.
Base URL and request format
https://www.dyli.io/api/commerce/v1
- OpenAPI 3.1 document
- Version:
v1.32(stable/v1endpoint paths) - Checkout shape: one product; gacha Boxes allow quantity
1–10, other types1 - Purchase delivery: the customer's DYLI Collection on Abstract
- Shipping: a separate redemption after purchase
- Quote lifetime: normally 10 minutes; a Stripe session may extend it to the Checkout expiry
Every request needs only its app-bound Commerce API key:
x-api-key: dyli_live_...
Authorization: Bearer dyli_live_... is also supported. Commerce credentials are accepted only in headers, never in query parameters or request bodies.
DYLI resolves the key's registered application and its isolated configuration, customers, quotes, orders, events, and idempotency namespace. GET /bootstrap returns the slug as partner.slug; there is no separate slug setting to copy. Older clients may still send x-partner-slug as an optional assertion, but a mismatch is rejected with commerce_app_mismatch. A Commerce key cannot select a different application, even under the same owner. Omitting the slug never creates a default account or falls back to the key's label.
Create a Commerce key
Create a Commerce key at dyli.io/requestapi with an active DYLI Pro developer account. Every Commerce endpoint, including GET, requires a dedicated Commerce key bound to an app. Free Read keys and legacy keys with commerce/write scope strings are not Commerce keys. Rotation preserves key type and application; it never upgrades Read access.
Registration collects your company, HTTPS domain, use case, auth, wallet and payment approach. Company names are unique after case/Unicode/whitespace normalization, and generated app slugs are globally unique. This reserves an app identifier; it is not a legal/trademark ownership verification. Select an existing app to create additional keys without re-registering its company.
Your setup is saved and you can download a tailored backend starter. Managed login is a separate Privy app set up by DYLI, not shared DYLI sign-in: request activation before enabling customer login. Creating a key doesn't create a test environment or fund a wallet. If you use your own login, you don't need Privy or a managed-login setup. A temporary HTTPS deployment you control is fine at registration; request a domain or login-choice change before switching an existing app.
Each account can have three active keys per type and 25 registered apps. Default limits are 30 requests/minute for Read keys and separate 300-read / 120-write requests/minute buckets for Commerce. Operator overrides can change these limits; honor 429 and Retry-After. Commerce entitlement is checked on requests, so an expired Pro membership stops Commerce access. An enabled, unexpired Commerce key whose owner’s Pro membership has expired can still use public Read endpoints at the free limit.
Check available features
Call the API root before enabling checkout:
curl "https://www.dyli.io/api/commerce/v1" \
-H "x-api-key: $DYLI_API_KEY"
The response tells you which features and payment methods are available for your app:
{
"environment": "production",
"capabilities": {
"writes": { "enabled": true, "status": "available" },
"payments": ["usdc", "stripe_card"],
"purchasing_ready": true,
"fulfillment": { "ready": true, "live_ready": true, "mode": "executor" }
},
"payment": {
"stripe_card": {
"available": true,
"hosted_checkout": true,
"embedded_checkout": true,
"webhook_configured": true
}
}
}
Only show a payment method that appears in capabilities.payments. Only enable purchase submission when capabilities.purchasing_ready is true.
Box purchase flow
From checkout to delivery:
- Read
GET /catalog/boxesand save the selected box'spurchase.quote_item. - Synchronize the customer and their Abstract wallet.
- Create a quote with the untouched
quote_item. - Complete USDC or Stripe payment.
- Complete the customer-wallet buy → results → decision → finalize flow below.
- Poll the order or receive signed webhook events until fulfillment is complete.
- When the customer wants physical delivery, create a redemption.
Catalog, quote, and payment data must be treated as server-authoritative. Do not calculate the payable amount or copy token/recipient addresses into application configuration.
Catalog
For box artwork, top pulls and opening results, follow the image sizing example. Use object-fit: contain to preserve the whole asset.
Catalog results are filtered by the partner's saved configuration and do not expose DYLI-internal identity fields. Purchasable rows include an instruction like:
{
"purchase": {
"supported": true,
"action": "buy",
"reason": null,
"quote_item": {
"type": "box",
"box_id": 12997,
"quantity": 1
}
}
}
Pass purchase.quote_item to POST /quotes, keeping the box ID and type from the response. Set quantity to the customer's choice, up to the returned maximum of 10.
Use GET /catalog/boxes for boxes and GET /community/market for actionable peer listings and offers. The wider catalog also exposes these read surfaces for apps configured to access them:
| Method and path | Purpose |
|---|---|
GET /catalog/explore | Partner-filtered storefront inventory |
GET /catalog/listings | Primary and secondary buy-now listings |
GET /catalog/secondary-listings | Current secondary listings for one product/token |
GET /catalog/boxes | Boxes that can be purchased and opened |
GET /catalog/ebay | Approved external marketplace inventory |
GET /catalog/search | Cross-catalog search |
GET /catalog/collections | Active partner collections |
GET /catalog/fair-drops | Fair Drop discovery |
GET /catalog/digital-packs | Digital Pack discovery |
GET /catalog/redemptions?product_id={id} | Public redemption activity for one product |
GET /catalog/options | Allowed brand/category filter values |
Reading a catalog item doesn't mean it can be purchased. Check purchase.supported, purchase.action and purchase.reason. For secondary marketplace and P2P writes, use the community action flow, not box quote or checkout endpoints. eBay checkout is not included.
Customer identity and delivery wallet
Your application owns login and the stable external customer ID. A purchase customer needs an Abstract EVM wallet because assets are delivered to that wallet's DYLI Collection.
curl -X PUT \
"https://www.dyli.io/api/commerce/v1/customers/customer_123" \
-H "x-api-key: $DYLI_API_KEY" \
-H "content-type: application/json" \
-d '{
"wallet_address": "0xCUSTOMER_ABSTRACT_WALLET",
"wallet_chain": "abstract",
"email": "buyer@example.com",
"name": "Example Buyer",
"partner_auth": {
"provider": "your-auth-system",
"subject": "auth-user-123"
}
}'
The same wallet or partner-auth identity cannot be linked to two external customers in one partner namespace. Treat external_customer_id as permanent.
DYLI links the delivery wallet to an existing DYLI profile, or creates a profile
with a unique username such as collector_4821. Orders and pulls stay attached to
that profile. Existing usernames and privacy settings are preserved; customer
contact details are not used to generate public usernames.
If a short handle is taken, DYLI can add a word, such as mint_ripper_4821,
while keeping a four-digit suffix.
Your app's login remains separate from DYLI login. A matching email or a partner Privy ID does not grant access to a DYLI account or wallet.
The customer wallet and payment wallet are deliberately separate:
customer.wallet_addressis the Abstract destination for the purchased asset.payment.payeris the wallet that funded and authorized a USDC order.
This supports embedded wallets for customers and a partner-controlled treasury wallet for payment.
Create a quote
Quote, payment-session, order and redemption creation need an idempotency key. Reusing a key with the same request returns the prior result; reusing it with different input returns 409 idempotency_conflict. Box creation is idempotent by paid order_id; subsequent operations use the play ID.
curl -X POST "https://www.dyli.io/api/commerce/v1/quotes" \
-H "x-api-key: $DYLI_API_KEY" \
-H "Idempotency-Key: quote_customer_123_cart_456" \
-H "content-type: application/json" \
-d '{
"external_customer_id": "customer_123",
"items": [
{ "type": "box", "box_id": 12997, "quantity": 1 }
]
}'
Use the purchase.quote_item returned by the chosen box. For peer listings and offers, prepare a customer-signed community action instead. Additional purchase types in the schema do not imply that your app can use them.
The response includes the server-priced quote and current payment instructions:
{
"quote": {
"id": "4b975557-7d53-4d52-b240-40ceac59eead",
"external_customer_id": "customer_123",
"currency": "USDC",
"items": [],
"price_breakdown": {
"subtotal_cents": 2500,
"partner_fee_cents": 125,
"total_cents": 2625,
"subtotal": 25,
"partner_fee": 1.25,
"total": 26.25,
"currency": "USDC",
"fee_label": "Service fee"
},
"status": "open",
"expires_at": "2026-08-14T18:10:00.000Z"
},
"payment": {
"amount": "26.25",
"currency": "USDC",
"recipients": {},
"tokens": {},
"supported_chains": []
}
}
Prices and inventory are checked again during payment finalization. A successful quote is not an inventory reservation.
Storefront request protection
The Commerce example uses
POST /storefront/limits to check shared request budgets before making API calls.
Your server sends { "budget": "client-read", "subject_hash": "64-character lowercase hex hash" }
with its Commerce key. Hash the subject with HMAC-SHA-256 using your server key;
never send raw IPs, customer IDs or browser-supplied identities.
Budgets are client-read (90/min), client-write (40/min), customer (90/min),
upstream-read (220/min) and upstream-write (80/min). Upstream budgets apply to
the whole key regardless of subject. This separate lane is capped at 1,800 checks
per minute and doesn't consume normal Commerce read/write quota. It returns
allowed: true, or 429 with Retry-After. On 503, stop the protected request;
never fall back to unprotected checkout. No extra database credentials are needed.
Count real upstream calls, not only browser requests, and use verified customer IDs for customer limits. Add hosting-level WAF limits and alerts too: these checks protect your API quota but do not stop traffic from reaching your hosting bill.
USDC payment
USDC payment supports Abstract, Avalanche, Ethereum, Base, Arbitrum, Optimism, Polygon, HyperEVM, Monad, and Solana. The quote response provides the current recipient and token addresses for each chain.
The API requires both onchain payment evidence and a signature from the wallet that funded the payment. This binds the payment to the partner, quote, customer/fulfillment intent, chain, payer, transaction hash, and quote expiry.
Accept other crypto
Your customer doesn't have to start with USDC. Your checkout can accept crypto on other networks through a swap or bridge provider such as Relay. You choose the payment experience; DYLI verifies the final settlement. Available currencies and routes depend on your provider. Conversion isn't built into the starter.
For a box storefront, a straightforward route is:
- Convert or bridge the customer's funds into Abstract USDC, delivered to their Abstract wallet or your treasury wallet.
- Wait for the funds to arrive. Use an open DYLI quote and send its exact total from that wallet to
payment.recipients.abstract, usingpayment.tokens.abstract. - Request and sign the payment authorization with that same payer wallet. Submit the final Abstract USDC transfer hash, payer and signature to create the order.
Allow for conversion fees and slippage so the payer has enough USDC. Check quote expiry before sending; if it expires during conversion, get a fresh quote and confirm any price change with the customer. Save conversion and payment references so an interrupted checkout resumes without charging again.
A swap receipt or source-chain transaction hash isn't DYLI payment evidence. Don't send a bridge payout straight to DYLI unless its USDC transfer sender can authorize the order: a relayer or bridge contract may be the sender, not your customer. Funding a wallet you can sign with first avoids that mismatch. Once you've sent the DYLI payment, recover that payment/order rather than making another transfer.
Partner treasury wallet
A partner may collect money from customers in its own checkout or ledger and pay DYLI from a pooled treasury wallet. This works without moving the purchased asset through the treasury wallet:
- Your system charges or debits the customer using your own payment arrangement.
- Your treasury sends the exact quote total in USDC to the chain-specific recipient returned by DYLI.
- The treasury signs the DYLI authorization message.
- Your server sends the transaction hash, treasury address, and signature to DYLI.
- DYLI fulfills the asset to
customer.wallet_address, not to the treasury.
This is a noncustodial treasury flow, not a prefunded balance held by DYLI. Your system remains responsible for customer charges, refunds, ledgering, and reconciliation. Never use one transaction for multiple DYLI orders; a payment transaction can be consumed once.
Authorize and create the order
After the transfer is confirmed, request the exact message the payer must sign:
curl -X POST \
"https://www.dyli.io/api/commerce/v1/quotes/$QUOTE_ID/payment-authorization" \
-H "x-api-key: $DYLI_API_KEY" \
-H "content-type: application/json" \
-d '{
"external_customer_id": "customer_123",
"customer": {
"external_customer_id": "customer_123",
"wallet_address": "0xCUSTOMER_ABSTRACT_WALLET"
},
"fulfillment": { "mode": "vault" },
"payment": {
"chain": "base",
"tx_hash": "0xPAYMENT_TRANSACTION_HASH",
"payer": "0xPARTNER_TREASURY_WALLET"
}
}'
Sign authorization.message exactly as returned:
- EVM externally owned account: EIP-191
personal_sign - EVM smart wallet: EIP-1271 contract signature
- Solana: Ed25519
signMessage, encoded as base58 or base64
Then create the order with the same customer, fulfillment, chain, transaction, and payer:
curl -X POST "https://www.dyli.io/api/commerce/v1/orders" \
-H "x-api-key: $DYLI_API_KEY" \
-H "Idempotency-Key: order_customer_123_checkout_456" \
-H "content-type: application/json" \
-d '{
"quote_id": "4b975557-7d53-4d52-b240-40ceac59eead",
"external_order_id": "your-order-456",
"external_customer_id": "customer_123",
"customer": {
"external_customer_id": "customer_123",
"wallet_address": "0xCUSTOMER_ABSTRACT_WALLET",
"email": "buyer@example.com"
},
"fulfillment": { "mode": "vault" },
"payment": {
"chain": "base",
"tx_hash": "0xPAYMENT_TRANSACTION_HASH",
"payer": "0xPARTNER_TREASURY_WALLET",
"payer_signature": "0xSIGNED_AUTHORIZATION_MESSAGE"
}
}'
For Solana, the transaction parser verifies a parsed SPL-token transfer from token accounts owned by the signed payer into the configured recipient. For EVM chains, DYLI verifies successful USDC Transfer logs, recipient, amount, sender, and confirmations.
Card payment
Stripe Checkout is created from an open quote. DYLI adds a 3% plus $0.33 card-processing fee to the quoted total.
Hosted Checkout:
curl -X POST \
"https://www.dyli.io/api/commerce/v1/quotes/$QUOTE_ID/stripe-checkout" \
-H "x-api-key: $DYLI_API_KEY" \
-H "Idempotency-Key: stripe_customer_123_checkout_456" \
-H "content-type: application/json" \
-d '{
"external_customer_id": "customer_123",
"external_order_id": "your-order-456",
"customer": {
"external_customer_id": "customer_123",
"wallet_address": "0xCUSTOMER_ABSTRACT_WALLET",
"email": "buyer@example.com"
},
"fulfillment": { "mode": "vault" },
"ui_mode": "hosted",
"success_url": "https://store.example/checkout/success?session_id={CHECKOUT_SESSION_ID}",
"cancel_url": "https://store.example/checkout/cancelled"
}'
Redirect the browser to checkout.checkout_url. For embedded Checkout, send ui_mode: "embedded" and an HTTPS return_url, then use the returned client_secret and publishable_key with Stripe.js.
Use the customer's current page for return URLs. DYLI accepts registered HTTPS origins and loopback navigation (localhost, 127.0.0.1, [::1], including development ports). Do not trust arbitrary Host headers or unregistered remote domains. Local returns do not change payment mode; production payments remain real. Your login provider must separately allow the local origin.
DYLI finalizes paid sessions from the Stripe webhook. The success page should also call your server, which may call:
POST /stripe-checkouts/{payment_session_id}/confirm
POST /stripe-checkouts/{payment_session_id}/cancel
GET /stripe-checkouts/{payment_session_id}
GET /stripe-checkouts/resolve?stripe_session_id={CHECKOUT_SESSION_ID}
The resolve endpoint recovers the opaque Commerce payment-session ID after a hosted redirect when browser storage is unavailable. Resolve first, then call confirm. Confirmation and webhook paths independently retrieve and verify the Stripe Checkout Session and PaymentIntent. A browser redirect by itself is never accepted as payment evidence.
If the customer abandons an unpaid checkout, call cancel after verifying customer ownership. DYLI expires the session at Stripe and returns cancelled: true. Only then clear local recovery and create a new quote. Completed or processing payments return 409 checkout_not_cancellable; provider/network uncertainty also leaves recovery intact. Cancellation is repeatable, is not a refund, and never discards a paid order.
Orders and asynchronous fulfillment
Paid order creation returns an order immediately. Fulfillment is asynchronous.
GET /orders/{order_id}
GET /orders?external_customer_id=customer_123&limit=25&offset=0
Important fields:
| Field | Meaning |
|---|---|
status | Overall state: normally paid, processing, completed, or failed |
fulfillment_status | Queue/executor state |
delivery.wallet_address | Abstract wallet receiving the item |
payment.sender | Verified funding wallet |
dyli_order_ids | DYLI fulfillment records after processing |
error | Structured action/retry information when present |
Do not mark an order fulfilled in your application only because payment succeeded. Wait for fulfillment_status: "completed" or an order.completed webhook.
How it appears in DYLI
Every checkout is saved as a Commerce order. DYLI also links the customer wallet to a DYLI user:
- If the wallet already belongs to a DYLI user, the order uses that user.
- Otherwise, DYLI creates a wallet-linked profile with a unique generated username. This does not create a DYLI login session or link the customer's other logins.
Before fulfillment can complete, DYLI verifies that the normal DYLI order exists, succeeded, and belongs to that customer. The purchase then appears in the customer's Collection and DYLI sales/activity like a purchase made on dyli.io. The normal order IDs are returned in dyli_order_ids.
Boxes
One paid Box order opens 1–10 units of the same live gacha Box. Other checkout types still accept quantity 1. Read capabilities.box_play.ready, max_quantity, chain_id, and the catalog row's purchase.supported before enabling payment. Retired flip/dabble contracts and restocking Boxes are not purchasable through this flow.
The partner's payment is verified once. The customer's two gacha transactions have zero native value and do not debit the Box price again. Gas still needs sponsorship or native funds. DYLI's canonical gacha engine draws the rewards, validates receipts and settles real holdings; partners must not generate outcomes or infer ownership from an animation.
| Step | Request / action | Result |
|---|---|---|
| Purchase | Quote { "type": "box", "box_id": 123, "quantity": 3 }, then complete Stripe or USDC payment | One paid Commerce order |
| Prepare | POST /box-plays with { "order_id": "…" } | box_play.id, quantity, opening_reference, and transaction (phase: "buy") |
| Buy | Customer wallet sends the returned transaction, on its chain_id | Persist the buy transaction hash immediately |
| Draw | POST /box-plays/{id}/results with { "tx_hash": "0x…" } | Poll when ready: false, respecting retry_after_ms; when ready, show ordered box_play.rewards[] |
| Choose | POST /box-plays/{id}/decision with { "decisions": ["claim", "sell_back", "claim"] } | Exactly one choice per reward; returns finalize transaction when ready: true |
| Settle | Send that transaction, then POST /box-plays/{id}/finalize with { "tx_hash": "0x…" } | Receipt-verified completed, real dyli_order_ids, and updated holdings / buyback balance |
Each reward includes index, product_id, token_id, product, rarity, and buyback_amount in USD. Offer sell-back only for a positive buyback. claim is the API value for “Vault.” Card-funded buybacks retain DYLI's card-risk withdrawal holds. A partner-treasury-funded purchase is USDC funded at DYLI, even if the partner independently accepted a card.
Claims and buyback USDC settle to the registered customer wallet. They do not automatically credit a partner treasury or a separate app ledger. Reconcile confirmed settlement before updating your own balance; a reveal or a selected Sell button is not payment evidence.
sell_back applies while settling the opening. After a claim has been finalized, use the post-vault DYLI offers flow. This does not change the original opening's disposition. The public Read API's /offers remains read-only and does not generate customer-specific buybacks.
On DYLI, eligible paid gacha claims retain their opening's buyback price until 48 hours after the claim is recorded, while the customer still owns the item. Loading offers does not restart this window. Separate standing DYLI marketplace offers use current pricing and eligibility rules and have their own expiration dates, commonly 90 days; they are not a guaranteed renewal of the initial offer.
POST /box-plays is naturally idempotent by paid order_id: one order authorizes one batch. Never create another paid order just because an opening timed out. 409 box_play_busy means another request holds the order lock; retry with backoff. Both results and decision can return HTTP 200 with ready: false, restart: true, and recovery.next_action when the block window expires. Follow that action: call create again with the same paid order. Do not keep polling the old decision. A restart returns a new opening_reference; clear old buy/finalize hashes and choices when it changes, submit the returned prepaid buy transaction, then request results again. This restarts the opening, not the payment (recovery.requires_new_payment: false). GET by play ID also exposes recovery instructions. If recovery.code is results_required, confirm the existing buy using the supplied results action instead of restarting it.
Persist the order/play IDs, opening reference, decisions, and both transaction hashes before subsequent network requests. Use GET /box-plays?external_customer_id=... and GET /orders?external_customer_id=...&external_order_id=... for recovery on another device. Repeating POST /box-plays resumes the existing purchase. If finalize was already broadcast, submit its hash to /finalize; do not send a second transaction. /decision can also accept tx_hash to recover a known finalize receipt.
Customers whose storefront login shares their DYLI account and wallet can also resume an existing paid opening on DYLI's page for that Box. DYLI verifies the logged-in user, registered wallet and original paid order, reuses the same purchase, and records completion for the partner storefront. An expired opening can restart without another payment. Separate storefront identities need to recover through the storefront's API flow above.
The old /commit path is an alias of /results only. There is no automatic claim, no defer_decision behavior, and no compatibility with retired contract calldata. Existing legacy plays need operator reconciliation, not replay through gacha.
Never rebuild calldata, replace the from wallet, or change target/value. Validate network and wallet, then treat each transaction as an opaque instruction. GET reads do not prepare signatures or send transactions.
Post-vault DYLI offers
Check capabilities.post_vault_offers.ready before enabling this flow. It supports initial claim buybacks and later available DYLI standing offers. For general collector offers, use marketplace actions. Your backend must authenticate the customer and supply their synchronized external_customer_id; never expose the Commerce key to the app.
- Discover:
POST /offers/querywith{ "external_customer_id": "customer-123", "token_id": "456" }. This uses the same customer-specific offer discovery as DYLI and may create a buyback offer, so it requires write scope and uses the write rate limit. It does not sell the item. - Prepare:
POST /offer-acceptanceswith anIdempotency-Keyheader and{ "external_customer_id": "customer-123", "token_id": "456", "offer_id": "789", "expected_amount": "75000000" }. Copyoffer_idandexpected_amountfrom the selected offer'sidandamount. This checks ownership, price, expiry, current on-chain bid and normal DYLI sale restrictions. - Approve if needed: if
acceptance.approval_transactionis present, have the customer sign it and wait for success. This grants the DYLI marketplace ERC-1155 operator approval; explain that permission in your wallet UI. It is not a sale or a USDC payment. - Accept: have the customer sign
acceptance.transactionexactly as returned. It callsacceptBidon Abstract. Persist the acceptance ID and transaction hash immediately. The transaction must be mined beforeexpires_at; preparation does not reserve the public bid or extend its deadline. Gas must be funded or sponsored by your wallet infrastructure. - Confirm:
POST /offer-acceptances/{id}/confirmwith{ "tx_hash": "0x..." }. DYLI verifies the receipt, customer, marketplace, offer, quantity, price and block timestamp, then runs the normal DYLI settlement and records the resulting order.acceptance.status: "completed"means this recording is complete.
offers[] includes id, token_id, price (USDC units), amount (integer USDC base units, six decimals), currency, maker_wallet, expiration (Unix seconds), expires_at (ISO), quantity: 1, and type: "claim_buyback" | "standing_buyback". Discovery returns eligibility: "requires_preparation"; successful preparation changes it to eligible. Availability can change before the wallet transaction is mined. No offers is a valid result; an upstream discovery failure returns an error rather than an empty success.
The existing DYLI settlement rules also apply here: eligible box inventory follows its normal restocking path, owner-box reservations/holds and applicable adjustment accounting remain in place, and card/GIMME5 withdrawal holds still apply. Proceeds settle through the existing marketplace to the customer wallet. These endpoints do not add a partner fee or payout-recipient override; fee_rules and recipient overrides are rejected.
Recovery: use GET /offer-acceptances/{id} or GET /offer-acceptances?external_customer_id=customer-123 (limit/offset supported). Reusing the same idempotency key and input returns the same acceptance; different input returns 409 idempotency_conflict. 409 acceptance_exists includes your existing acceptance_id. 409 offer_busy means retry shortly. Re-query and prepare a new offer if the bid expires before submission.
If the sale was already broadcast, confirm that same hash even after the 48-hour deadline or after the item leaves the wallet. Eligibility at the mined block matters. requires_action / 503 acceptance_reconciliation_required means the on-chain sale succeeded but DYLI recording needs a retry: call confirmation again with the same hash, never submit another sale. Completed confirmations are idempotent; a different hash is rejected. Prepared records past their deadline read as expired with no transaction instructions, but timely mined transactions can still be confirmed. Poll these resources for recovery; no new offer-specific webhook is emitted.
Your own domain or mobile checkout
Privy is a starter choice, not a Commerce requirement. Your backend authenticates your user and binds their stable external ID to their Abstract wallet. It keeps the DYLI key; the browser or mobile app never receives it. A native app can use its own wallet/signing SDK and render only its partner-filtered Boxes.
Use DYLI-hosted Stripe for the smallest payment integration (Apple Pay availability is controlled by Stripe/device eligibility). To use your own Apple Pay, card processor, or app balance, verify/debit it on your backend, then use the partner-treasury USDC flow above. A client paid: true, Apple Pay token, arbitrary provider receipt, or unverified webhook is not DYLI settlement. Your backend owns payment verification, refunds, disputes, idempotency and reconciliation. Inventory can disappear after payment because a quote is not a reservation; reconcile failed paid orders rather than charging again.
To build an experience focused on selected boxes, set catalog_rules.allowed_box_ids to their gacha product IDs and disable other catalog surfaces in PATCH /config. Turn off community_settings.marketplace_enabled and community_settings.trading_enabled if you do not want those features. The example's NEXT_PUBLIC_BOXES_ONLY=true limits primary-catalog navigation; it does not override enabled marketplace or trading capabilities. DYLI checks catalog rules again when quoting, so UI filtering alone is not access control. Test your settings before publishing your domain.
Shipping and redemptions
Items the customer keeps go into their vault, called Collection in the API. To ship eligible items, create a redemption from that vault. This is a separate shipping checkout, not another purchase of the item.
Redemption items use the Collection token_id, not the internal product ID. Addresses use the normalized address_line_1, address_line_2, and two-letter country_alpha2 field names returned by address validation.
{
"external_customer_id": "customer_123",
"items": [{ "token_id": "67890", "quantity": 1 }],
"address": {
"name": "Example Buyer",
"email": "buyer@example.com",
"phone": "+1 555 555 0100",
"address_line_1": "123 Main Street",
"address_line_2": null,
"city": "New York",
"state": "NY",
"postal_code": "10001",
"country_alpha2": "US"
}
}
| Method and path | Purpose |
|---|---|
POST /redemptions/address-validation | Normalize and validate the address |
POST /redemptions | Verify Collection ownership and quote live shipping rates |
GET /redemptions?external_customer_id={id}&limit=100&offset=0 | List and page a customer's redemption history |
GET /redemptions/{id} | Read the redemption state |
POST /redemptions/{id}/prepare | Reprice shipping, select rates, and return an Abstract transaction |
POST /redemptions/{id}/confirm | Verify the customer transaction and create fulfillment records |
Confirm any recommended address correction with the customer before continuing. A redemption can contain one or multiple eligible items. Submit the selected rate and any item-specific collection responses at prepare time, then send the exact returned transaction from redemption.wallet_address. Confirm the transaction before showing the shipment as created.
Shipping is paid in USDC on Abstract. Use the returned payment.token_address, payment.spender, and payment.amount_cents to check the customer's balance and token allowance before submitting the transaction. There is no Stripe shipping-checkout endpoint in this flow.
Poll the individual redemption or the customer's redemption list for current fulfillment details in result.orders. Each order includes shipment_id, carrier, tracking_number, tracking_url, shipment_status, delivered, and tracking_unavailable. Carrier data can be cached for up to 15 minutes. tracking_updated_at is included when provider data was retrieved. status: completed means the redemption was processed, not that the parcel was delivered; use delivered for carrier-confirmed delivery. A null delivery value means it has not been established. If a provider is temporarily unavailable, known tracking is retained and tracking_unavailable is true. Manual shipments show pending or tracking_available as staff add tracking.
For Easyship shipments, tracking_url opens the shipment's tracking page, even before a carrier tracking_number is assigned. Use shipment_id (for example, ESUS360467167) as the link text and show the carrier number separately when available. Multiple items can share one shipment_id; show one tracking entry per package. When fulfillment is split, the shipments array contains each package's tracking details. Only label the whole request delivered when all its packages are delivered.
Use each selected option’s courier_id when present, otherwise its id: for example, {"shipping_selection":{"manual":"courier-service-id"}}. DYLI matches the same service when rates are refreshed; it won't substitute another carrier. If that service disappears, prepare returns shipping_option_unavailable. Show fresh options and let the customer choose again.
Shipping quotes last 10 minutes. Preparing a shipment reprices delivery and returns a new expires_at with a full payment window. Show the returned total before payment. If an unpaid quote expires, create a new quote with the same items and address and a new idempotency key. Never restart a shipment that has a transaction hash or an uncertain submission: read its state and confirm the original transaction instead. A confirmed payment can still be reconciled after the quote’s expiry.
Partner configuration and webhooks
Read or update partner configuration:
GET /config
PATCH /config
Configuration includes display_name, branding, catalog_rules, fee_rules, community_settings, and webhook_url. A box fee can be percentage, fixed, or fixed-plus-percentage with optional minimum/maximum limits. Community settings control marketplace and P2P discovery separately.
Zero partner fees are supported. Send {"fee_rules":{"enabled":false}} to PATCH /config. An enabled fixed fee of 0 also works when minimum is null or 0. Fixed fees are per order by default for compatibility; set per_unit: true to multiply the fixed amount by box quantity. Percentages use the full box subtotal. Always render the quote's calculated partner_fee_cents; disabling partner fees does not waive box price, processing, gas or shipping.
Your backend may instead include fee_rules in POST /quotes for a single
checkout, without changing the account configuration:
{
"external_customer_id": "customer_123",
"items": [{ "type": "box", "box_id": 12997, "quantity": 2 }],
"fee_rules": { "enabled": true, "type": "fixed", "fixed_amount": 2, "per_unit": true, "label": "Your fee" }
}
That adds $4. Use type: "percent", percent: 5 for 5% of subtotal instead.
An explicit enabled: false overrides a nonzero account fee. Omission inherits
the account rules. DYLI snapshots the rules on the quote and includes an override
in its idempotency hash. price_breakdown.fee_rules records those rules;
fee_quantity shows per-box fixed fees in both Stripe and the starter. Card and
USDC collect the full quote total. Fee input is trusted partner server policy,
never customer form data. Starter env: FLAT_FEE, PERCENT_FEE, INCLUDED_IDS;
see the quickstart.
Commerce reporting and earnings
GET /analytics?offset=0&limit=25 is a private server-to-server report scoped to
the Commerce key's application. It returns summary, boxes, paginated
orders, applications, share_percent: 10, and as_of. Summary fields include
orders, completed_orders, boxes_opened, sales, buybacks, buyback_count,
item_costs, profit, profit_share, your_fee, and earnings. Monetary values are
USD/USDC decimal amounts. Totals cover all history, not just the current page.
Orders contain quantity, box art, application/key IDs and earnings_status.
This is owner financial data; do not proxy it to storefront customers.
The key owner's DYLI Storefront → Commerce tab aggregates all their apps,
including rotated/revoked keys. Profit is settled base sales minus cash buybacks
and the cost of items customers keep. Each kept item uses its order’s saved
dabble_cost when positive, otherwise its product price, rounded to cents.
An item sold back incurs the cash buyback only,
not an additional item cost. item_costs is included in summary, box and order
rows. Orders with an unresolved kept-item cost remain pending for earnings.
The automatic share is 10% of settled box profit, with losses carried
against gains before the owner's share is floored at zero. Added fees are not in
that profit base and are credited separately as Your fee. Per-box share rows
are signed contributions; an individual app/box subtotal is not a separate
claimable balance. Verified completed gacha orders only; unfinished, failed,
refunded, cancelled, test and complimentary orders earn nothing.
These earnings use the existing DYLI Claim verification/reservation and withdrawal history, not a new payout endpoint. History is attributed to the key owner; the customer's login identifies who receives the box, not who earns the Commerce share. Refreshing a report or retrying a finalized order cannot create another credit.
Commerce earnings is a lifetime earned amount, not a second wallet or another claimable balance. The Claim flow subtracts existing withdrawal reservations.
Webhooks
Setting a webhook URL for the first time, or sending rotate_webhook_secret: true, returns webhook_secret once. Store it immediately.
Webhook requests include:
x-dyli-event-id: EVENT_UUID
x-dyli-event-type: order.completed
x-dyli-timestamp: 1786720800
x-dyli-signature: v1=HEX_HMAC_SHA256
Verify HMAC-SHA256(secret, timestamp + "." + raw_request_body) with a constant-time comparison. Reject stale timestamps and deduplicate by x-dyli-event-id before changing state. Respond with a 2xx quickly; process the event asynchronously.
Use GET /events for reconciliation. Event delivery is at-least-once, so consumers must be idempotent.
Retries, limits, and errors
- Send
Idempotency-Keyfor quote, order, Stripe Checkout, and redemption creation. - Persist the key and request body together until the operation reaches a terminal state.
- Retry
409 payment_pendingafter the chain confirms. - Retry
429after theRetry-Afterheader. - Retry
502/503with capped exponential backoff and jitter. 504 upstream_timeoutmeans an internal service exceeded its response deadline, not that the request was invalid. For reads, wait forRetry-Afterand retry with capped exponential backoff and jitter. For writes, check the operation's status before retrying and preserve its original idempotency key and body.- Do not automatically retry validation errors or idempotency conflicts with the same payload.
- Log the
x-request-idresponse header andrequest_iderror field for support.
Errors use this shape:
{
"error": "payment_pending",
"message": "Payment needs more confirmations",
"details": { "confirmations": 0, "required_confirmations": 1 },
"request_id": "7473e546-60b3-4d30-8520-4accbf5a57df"
}
Commerce keys have an in-process API limit plus a durable per-key read/write limit. Rate-limit headers are included in responses. Request a limit review before a planned traffic increase; include expected read/write traffic and launch date.
Testing
Follow the launch checklist for payments, openings, vaulting and shipping. The starter uses production by default, even on localhost. Ask about an isolated lab or launch test; if DYLI supplies a lab, use its assigned URL and key together. Test payments are rejected in production.