Sell an offer with checkout
Checkout buys an offer from your own page. It is one browser request, POST /edgekit/v1/checkout, that takes an offer
token, a card token and the buyer’s email address. EdgeKit charges the card through your Recurly account, then writes
the grant. There is no hosted checkout page: your page renders the form.
Card numbers never reach EdgeKit. Recurly.js on your page turns the card into a token, and checkout accepts only that
token. A request body carrying any key other than the six below is refused with 400, so a form that sends number
or cvv by mistake fails loudly.
What checkout needs
Section titled “What checkout needs”- Recurly as your active billing provider. EdgeKit holds your organization’s Recurly API key and charges with
it. Without the key, or with no active billing provider, every checkout answers
409 no_billing_provider. With another billing provider active, checkout answers409 billing_provider_unsupported. - An offer that can be sold: Purchasable,
active, with an active Recurly price mapped to it, and a billing provider coupon code if it has an introductory discount. See Create and sell an offer. - An outcome that sells it: an
offer_tokenfield naming the offer, on an outcome a feature returns, in Revenue › Decision Engine › Outcomes, deployed. - Your page’s origin in the site’s Allowed origins. Checkout is checked against the origin like every other browser request. See Site settings.
- Recurly.js on your page. Your Content Security Policy must allow it. The
createClientreference lists the hosts.
Checkout is served on the /edgekit/ mount only. There is no API-key twin for servers. See
First-party mount and site proxy.
1. Get an offer token
Section titled “1. Get an offer token”Ask for the feature as usual. When the decision returns an outcome with an offer_token field, the field’s value is
a fresh offer token.
import { createClient } from '@edgekitcloud/client-browser';
const client = createClient({ siteId: 'site_example' });const { paywall } = await client.decide('paywall');
let offerToken: string | undefined;
if (paywall.status === 'decided' && paywall.outcome === 'subscribe') { // `checkout` is the name you gave the outcome's offer_token field. offerToken = (paywall.value as { checkout: string }).checkout;}An offer token names one offer on one site. On its own it authorizes nothing: it only says which offer the buyer was
shown. It stays valid for 12 hours by default and can be sent again until then, so a declined card can be retried
with the same token. A token used on another site answers 409 offer_token_unknown.
2. Take the card
Section titled “2. Take the card”Load Recurly.js with your Recurly public key, render its card fields, and tokenize them when the buyer selects Buy. You get a card token id. It lives about 20 minutes and can be reused inside that time.
3. Call checkout
Section titled “3. Call checkout”// Your Recurly.js call, returning the card token id.const recurlyTokenId = await tokenizeCard();
const result = await client.checkout({ offerToken: offerToken!, // from step 1 recurlyTokenId, email: 'ada@example.com', firstName: 'Ada', // optional lastName: 'Lovelace', // optional});
if (result.status === 'ok') { // 'granted', 'charged' or 'pending'. The buyer may have paid in every case. showReceipt(result.data.status);} else if (result.status === 'failed') { handleRefusal(result.code, result.details);} else { showTryAgainLater();}curl -i https://your-tenant-host/edgekit/v1/checkout \ -H 'x-edgekit-site-id: site_example' \ -H 'Origin: https://www.example.com' \ -H 'Content-Type: application/json' \ --data '{"offerToken":"<offer token>","recurlyTokenId":"<card token>","email":"ada@example.com"}'The body takes these keys and no others:
| Key | Required | What it is |
|---|---|---|
offerToken |
Yes | The offer token from the decision. |
recurlyTokenId |
Yes | The card token id from Recurly.js. |
email |
Yes | The buyer’s address. It decides which user gets the grant. |
firstName, lastName |
No | Up to 100 characters each. Written only when checkout creates the user. |
threeDSecureActionResultTokenId |
No | Sent only to finish a 3-D Secure challenge. See 3-D Secure. |
checkout has no timeout: it waits for the card authorization. An AbortSignal passed as signal stops waiting
but does not cancel the charge. Nothing is retried for you. See the
CheckoutRequestBody and
CheckoutResponseBody reference.
4. Handle the answer
Section titled “4. Handle the answer”| Status | Body | Meaning | Browser client |
|---|---|---|---|
200 |
{ "status": "granted", "attemptId" } |
Charged, and the grant is written. | ok |
202 |
{ "status": "charged", "attemptId" } |
Charged. The grant is still being written, and EdgeKit finishes it. Show a receipt. | ok |
202 |
{ "status": "pending", "attemptId", "runId" } |
The charge’s outcome is not known yet. EdgeKit resolves it in the background. Show “still processing”. | ok |
400 |
bad_request |
A missing or malformed field, such as an invalid email, or a key checkout does not accept (named in the message). | throws |
403 |
forbidden |
The page’s origin is not allowed for the site. | throws |
404 |
not_found |
The site is unknown, disabled or not yet deployed. | throws |
409 |
one of the codes below | A precondition failed. Fix it and try again. | failed |
422 |
Recurly’s own decline code, or declined |
The card was declined. Show the message and let the buyer try another card. | failed |
5xx |
an error, such as internal_error |
No answer. The buyer may still have been charged. | unavailable |
Never show a 200 or a 202 as a failure, and never invite the buyer to buy again after pending: money may have
moved. Treat a status you do not recognize as pending. Every 200 and 202 carries attemptId; quote it to
support.
The browser client throws EdgeKitConfigError on 400, 403 and 404, because each means the page is wired
wrongly.
409 codes
Section titled “409 codes”| Code | Message | What to do |
|---|---|---|
offer_token_unknown |
That checkout link is not valid. | Decide again for a fresh token. |
offer_token_expired |
That checkout link has expired. Reload the page and try again. | Decide again for a fresh token. |
offer_unknown |
That offer is no longer available. | The deployed catalog no longer has the offer. |
offer_not_purchasable |
That offer is not for sale. | Tick Purchasable and deploy. |
offer_not_active |
That offer is no longer for sale. | The offer is archived. |
offer_discount_unsupported |
That offer cannot be bought here right now. | Add the billing provider coupon code and deploy. |
offer_not_priced |
That offer cannot be bought here. | No active price for your billing provider is mapped. |
no_billing_provider |
This site is not selling right now. | No active billing provider, or no Recurly key. |
billing_provider_unsupported |
This site is not selling through a supported billing provider. | The active billing provider is not Recurly. |
trial_already_used |
That trial has already been used on this account. | Offer a paid offer instead. |
email_invalid |
That is not an email address. | Ask for the address again. |
card_token_expired |
Your card details could not be used for this purchase. Re-enter them and try again. | Tokenize the card again. |
three_d_secure_required |
Your bank needs to verify this payment. Complete the check and the purchase will finish. | Run the challenge. See 3-D Secure. |
billing_provider_busy |
That purchase is already being processed. Try again shortly. | Wait for Retry-After (5 seconds) and send it again. |
billing_provider_rejected |
The billing provider refused this purchase. Nothing was charged. | Check the offer’s price and coupon in Recurly. |
attempt_possibly_charged |
A purchase on this link is still being resolved. Reload the page and try again in a few minutes. | Wait. EdgeKit is settling an earlier attempt. |
attempt_email_mismatch |
A purchase on this link is already in progress for a different email address. | Decide again for a fresh token. |
attempt_offer_repriced |
This offer has changed since that checkout link was created. Reload the page and start again. | Decide again for a fresh token. |
Branch on code. The messages are written for the buyer and may change.
3-D Secure
Section titled “3-D Secure”When the buyer’s bank wants them to authenticate, checkout answers 409 three_d_secure_required with
details.threeDSecureActionTokenId. This is not a decline.
- Run Recurly.js’s 3-D Secure challenge with
details.threeDSecureActionTokenId. - Call
checkoutagain with the sameofferTokenandrecurlyTokenId, plus the challenge’s result token asthreeDSecureActionResultTokenId.
Read details only after checking code.
Who gets the grant
Section titled “Who gets the grant”Checkout never reads the EdgeKit session. The grant goes to the user whose email address the buyer typed:
- If a user with that address exists, they get the grant. EdgeKit does not ask the buyer to prove the address, so a mistyped address gives the grant to whoever holds it.
- Otherwise checkout creates the user, with the address not yet verified, then charges. The buyer signs in with that address to use the access.
Addresses are compared after trimming and lower-casing, and nothing else: ada+news@example.com is a different user
from ada@example.com.
The user is created or found before the card is charged, in the same step that records the attempt, so a grant never waits for a user. The grant is the user’s own, never a user organization’s. It is not pinned to a site: it reaches every site the product covers.
Checkout does not refresh the copy of the user’s access that the browser holds: client.checkout() sends no
cookies. After checkout returns granted, a signed-in user’s page must read the session with
await client.id.getSession() (GET /edgekit/v1/id/get-session) before it decides again. Then the next decision
reads the new grant.
Trials
Section titled “Trials”When the offer is marked The offer is the trial, checkout applies your organization’s trial reuse rule before it
charges. By default a user gets one trial per product: buying a trial offer for a product the user already had a
trial of answers 409 trial_already_used. The rule is shown as Trial reuse under Configuration on each
site’s screen in Delivery › Delivery › Sites. The console marks it not read yet, but checkout reads
it.
Double clicks and retries
Section titled “Double clicks and retries”Requests with the same offer token converge on one purchase attempt, so a double click never charges twice. A second
request while the first is still in flight answers the first one’s result or 409 billing_provider_busy. A request
after the purchase settled answers the original result again and charges nothing, for up to an hour after the first
attempt. After a decline, send the same offer token again with the card the buyer enters next.
What checkout does not do
Section titled “What checkout does not do”- It does not take a coupon code from the buyer. An offer’s introductory discount applies to every buyer.
- It does not let the buyer choose a currency. It charges the offer’s first active price.
- It does not check the offer’s Eligibility or show Trial days: those are for your page.
- It does not sell through any billing provider but Recurly.
- It does not sell to a user organization.