Skip to content

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.

  • 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 answers 409 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_token field 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 createClient reference 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.

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.

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.

// 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();
}
Terminal window
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.

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.

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.

When the buyer’s bank wants them to authenticate, checkout answers 409 three_d_secure_required with details.threeDSecureActionTokenId. This is not a decline.

  1. Run Recurly.js’s 3-D Secure challenge with details.threeDSecureActionTokenId.
  2. Call checkout again with the same offerToken and recurlyTokenId, plus the challenge’s result token as threeDSecureActionResultTokenId.

Read details only after checking code.

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.

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.

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.

  • 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.