# Worthy Payments API > Accept card payments with fraud screening, acquirer routing, and 3-D Secure applied on every charge. Conventional REST API. All amounts are integers in the smallest currency unit (4999 is $49.99). Base URL: https://api.paygeyt.com/v1 · Auth: x-api-key header (sk_/pk_/rk_, test/live). The browser integration is the Payment Element: your server creates a payment intent with a secret key, and your page mounts our iframe against the returned client secret with a publishable key. The card never enters the merchant's page, which is what puts the integrator on PCI SAQ A (~30 requirements) instead of SAQ A-EP (~139). The amount is never sent from the browser. Building a charge on your own server means tokenizing first: `card.number` and `card.cvv` carry `ev:` tokens, or `card.wp_token` carries one `wp:v1:` envelope holding PAN, expiry and CVV together (so there is no `wp:` cvv field). A raw PAN is rejected. ## For agents (read this first) Do **not** scrape `/developers/api-reference`. It is a JavaScript UI and looks empty without a browser. Humans use that page. You use these files: 1. This index (`/developers/llms.txt` or `/llms.txt`, which redirects here) 2. The full corpus: `/developers/llms-full.txt` 3. Endpoint schema: `/developers/api-reference/openapi.yaml` Build a first payment in this order, then stop guessing: 1. [Quickstart](/developers/01-quickstart.md) 2. [Payment Element](/developers/08b-payment-element.md) 3. [Idempotency](/developers/12-idempotency.md) 4. [Webhooks](/developers/16-webhooks.md) 5. [Integration hard rules](/developers/27-integration-hard-rules.md) Join an order to a webhook: - **Preferred: read `data.object.detail.orderReference`** — your own reference, echoed back on the money events (`charge.authorized`/`succeeded`/`captured`/`failed`/`refunded` and their `transaction.*` twins). `detail` also carries the buyer (`billingName`, `billingEmail`, `billingLine1`…), the gateway's answer (`authCode`, `responseCode`, `avsResult`), `cardExpMonth`/`cardExpYear`, and `arn`/`settlementBatchId` once settlement reports them. - `detail` rides the money events above, every `subscription.*` event except `usage_exceeded`, every `invoice.*` event, and `customer.created`/`customer.updated`. It is **not** on `charge.canceled`/`transaction.voided` (which carries `orderReference` at the TOP level), on `transaction.updated`, or on either `charge.three_d_secure.*`. `dispute.*` and `pre_dispute_alert.*` nest their REST resource instead (`data.object.dispute`, `data.object.alert`) and are joined through `transactionId` — **except `dispute.funds_reinstated`, where only one of the six paths that return held funds fills the nested object, so read `data.object.dispute` as optional there and fall back to the `dispute.won` raised in the same transaction, or to `GET /v1/disputes/{disputeId}`**; `order.*` nests the order row at `data.object.order`; `customer.deleted` carries none. `customer.created`/`.updated` carry no transaction id at all — they are keyed on the customer id, and `detail` is the customer record. - Inside `data.object`, fields are **omitted when empty OR zero**, never nulled — that half of the body is protobuf rendered with the canonical JSON mapping, which drops every field still on its default. There is no `""`, no `0` and no `false` anywhere under `data.object`; the key is simply absent. Read `detail?.orderReference ?? ""`, `Number(x?.minorUnits ?? 0)`, `flag === true`. The envelope around it is ours and does not behave this way (`livemode` is a real `false` on a test delivery). `metadata` is still not echoed on any event. - `refundReason` is in the schema but is **never populated** — gateway-issued refunds carry no reason from the processor. Do not build on it. - **Fallback, if you set no `orderReference`:** persist the `POST /v1/payment_intents` `id` (`pi_…`) **and** `client_secret` against your order, then resolve with `GET /v1/payment_intents/{client_secret}` (the secret, not the `pi_…` id) and read `latest_charge` — that value is the webhook `transactionId`. `GET /v1/payment_intents/{id}` with only `pi_…` is a 404. - Ids on the wire are **camelCase**: `data.object.transactionId`; the snake_case `transaction_id` is `undefined`. - **`charge.succeeded` and `charge.captured` are one delivery moment, not two stages.** A captured sale fans out to `transaction.succeeded` + `charge.succeeded` + `charge.captured`, on an ordinary auto-capture sale and a cash sale as well as on auth-then-capture. An endpoint on both names gets every sale twice — pick one. `charge.authorized` is the only event that means funds are held and nothing has settled. - `X-Webhook-Id` is the **occurrence** id, equal to the envelope's own `id` and to `event_id` on the polled `/v1/events` item — NOT to that item's `id`, which is the per-endpoint delivery. `GET /v1/events/{X-Webhook-Id}` answers 404; `GET /v1/events/{item.id}` is the lookup. Subscriptions, invoices and orders carry the whole record, so a webhook-only integration need not call back: - `subscription.*` → **`data.object.detail`**: plan, status, quantity, `amount`, `interval`/`intervalCount`, `currentPeriodStart`/`currentPeriodEnd`, `billingTimezone`, `trialStart`/`trialEnd`, `cancelAtPeriodEnd`, `cancellationReason`, `collectionMethod`, `collectTax`, `couponId`, `currentUsage`, `createdAt`/`updatedAt`. - On `subscription.canceled`, **`cancelAtPeriodEnd` decides whether access ends now or at the period boundary**. `status: "CANCELED"` alone does not say. - `amount` is the **recurring base for the whole subscription** — the plan price × `quantity`, so 3 seats of a $29.99 plan read `"8997"`. It is not the settled charge: coupon discounts and downgrade credits are subtracted and tax is added when the renewal is charged. - `amount`, `interval` and `intervalCount` ride one priced line and **arrive or vanish together**. They are on all 8 `subscription.*` events that carry `detail`, and drop out only when no priced line is stored against the subscription. Five of the eight shipped none of them until recently. Absent means "not stated on this event", never "bills nothing". - **`dunningState` is always the literal `"NONE"`, and `dunningAttempts` is always a zero the payload omits.** `nextDunningAt` is absent too **except** after a merchant-triggered retry, where it is the PAST moment that attempt was claimed — never a future one we scheduled. Billing runs no retry clock — decline recovery is owned by the payments engine and reported on `decline_recovery.recovered`/`.failed`/`.hard_declined`. Do not read `"NONE"` as "nobody is retrying". `"RETRYING"` is not a member of this enum; the declared set is NONE, ACTIVE, FIRST_NOTICE, SECOND_NOTICE, FINAL_NOTICE, SUSPENDED. - **`nextBillingAt`, `endDate`, `externalId`, `invoiceDescription` and `taxRateId` are never populated** — no write path sets them, so they are absent from every delivery. For the next charge date read `currentPeriodEnd`. - `subscription.usage_exceeded` carries **no `detail`**: it sends `currentUsage`, `usageLimit`, `percentOver`. - `renewed`, `past_due` and `suspended` **also deliver as `subscription.updated`**. Subscribing to both names delivers each one twice. - `invoice.*` → **`data.object.detail`**: `invoiceNumber`, the full money breakdown (`subtotal`, `discountAmount`, `taxAmount`, `shippingAmount`, `total`, `amountPaid`, `amountRemaining`), `dueDate`/`finalizedAt`/`sentAt`/`paidAt`, `hostedInvoiceUrl`, `lines[]` and `lineCount`. - **Branch on `amountRemaining`, not `status`** — as `Number(detail.amountRemaining?.minorUnits ?? 0) === 0`. A partially paid invoice is not a paid one. The `?? 0` is mandatory: see the zero-money rule below. - `lines` is present on every invoice event and **capped at 200**; `lineCount` is the true total, so `lines.length < lineCount` means truncated. - Every money value is an object, never a bare number, and `minorUnits` is a JSON string. **A ZERO money arrives as `{"currencyCode":"USD"}` with NO `minorUnits` key** — zero is the default for an integer and the payload omits defaults. Always read `Number(m?.minorUnits ?? 0)`. This hits `amountRemaining` on every paid invoice, `taxAmount` on an untaxed one, `discountAmount` on an undiscounted one, `amountPaid` on `created`/`finalized`, and every zero money on `order.*`. **Not on `overdue`**: the overdue sweep fires on any invoice still `OPEN` with a balance past its due date, so a partially paid one is exactly what it picks up. - **`invoice.sent` carries a live `hostedInvoiceUrl` AND `checkoutUrl` at the TOP level** of `data.object`, beside `detail`. Both are complete bearer links — the same ones our invoice email sends your customer — so send them on, and treat the webhook body and anything you log it into as secret material. - **`detail.checkoutUrl` is always `""` and `detail.hostedInvoiceUrl` is token-stripped** on every invoice event, `invoice.sent` included. The checkout link is `…/checkout/` and that token IS the payment capability, carried as a path segment, so there is no token-free form to publish; `hostedInvoiceUrl` survives as a reference with its `?token=` removed. On `invoice.sent` the payable pair is at the top level instead. There is no public REST endpoint that returns a payable link — re-send the invoice from the dashboard. - `order.*` → **`data.object.order`**, the stored order record: line items, shipping/billing addresses, buyer contact, and — on the `.fulfilled` delivery that reports a tracking number — `trackingNumber` and `carrier`. There is no `GET /v1/orders/{id}` on the public API; orders are a dashboard surface. - Marking an order shipped and adding a tracking number are separate writes and each is a fulfilment fact: marking it shipped emits `order.fulfilled` with `order.trackingNumber` empty, and adding a tracking number the order did not already hold emits it again carrying the number. Either write can come first, and a tracking number that arrives with the shipping mark produces one delivery, not two. Key on the order id and treat the second as an update. Re-marking an already-shipped order, or re-sending a tracking number we already hold, emits nothing further. - `order.created`, `order.paid` and `order.completed` **all fire on one write**, at the same instant, on a row inserted already paid. Subscribe to one. - `events` and `digitalDeliveries` are **empty on the webhook**: the first is not loaded, the second is removed on purpose because a delivery summary carries a redeemable code and a signed download link. Neither is recoverable over the public API. - `shippingAddressJson` and `billingAddressJson` are **JSON strings**, not objects. - `payment_link.created|updated|deactivated` and `inventory.low_stock` are deliverable. `payment_link.updated` carries `changedFields`, the field NAMES rather than their values, so re-read the link when you need a value. - `store.*` and `product.*` are **not** deliverable: they record administrative changes we make, not changes you make. Create a plan with `POST /v1/plans` (or in the dashboard). `GET /v1/plans` lists it either way; pass the plan id on `POST /v1/subscriptions`. Email an OPEN invoice with `POST /v1/invoices/{id}/send` (`invoices:write`, server-only). There is no `invoice.payment_failed` event — a failed card renewal is `subscription.past_due`. If you already have the buyer's address, mount with `fields.billingDetails: 'never'` (or `{ address: 'never' }`) **and** `defaultValues.billingDetails`. Otherwise the charge has no AVS data. Style with `appearance` (colors, borders, radius, `boxShadow`, `rules['.Root']` for maxWidth and scroll). Apple/Google Pay mount on YOUR page above the iframe, not inside it. The iframe box is the host `#payment`. `position`, `display`, and `backgroundImage` are dropped. ## Guides - [Overview](/developers/README.md) - [Quickstart](/developers/01-quickstart.md) - [Authentication](/developers/02-authentication.md) - [Charges](/developers/03-charges.md) - [Checkout Links](/developers/04-checkout-links.md) - [Products](/developers/05-products.md) - [Storefronts](/developers/05b-stores.md) - [Refunds](/developers/06-refunds.md) - [Refund a customer's past charges](/developers/06b-refund-past-charges.md) - [3-D Secure](/developers/07-3ds.md) - [Embedded SDK](/developers/08-embedded-sdk.md) - [Payment Element](/developers/08b-payment-element.md) - [WooCommerce](/developers/09-woocommerce.md) - [Errors](/developers/10-errors.md) - [Health](/developers/11-health.md) - [Idempotency](/developers/12-idempotency.md) - [Pagination](/developers/13-pagination.md) - [Rate limits](/developers/14-rate-limits.md) - [Testing](/developers/15-testing.md) - [Webhooks](/developers/16-webhooks.md) - [Authorize & Capture](/developers/17-auth-capture.md) - [Customers & Saved Cards](/developers/18-customers.md) - [Subscriptions](/developers/18b-subscriptions.md) - [Invoices](/developers/18c-invoices.md) - [Events](/developers/19-events.md) - [Versioning](/developers/20-versioning.md) - [Going Live](/developers/21-going-live.md) - [Libraries & SDKs](/developers/22-libraries.md) - [Fraud checks](/developers/23-fraud-checks.md) - [Device data collection](/developers/24-device-data-collection.md) - [Integration modes](/developers/25-integration-modes.md) - [Risk decision token](/developers/26-decision-token.md) - [Integration hard rules](/developers/27-integration-hard-rules.md) - [Card tokenization](/developers/28-card-tokenization.md) - [wp: rail reference](/developers/29-sdk-reference.md) - [Tokenization error codes](/developers/30-error-codes.md) - [Key rotation](/developers/31-key-rotation.md) - [Migrating from ev:](/developers/32-migration-from-ev.md) - [Metrics](/developers/33-metrics.md) - [Changelog](/developers/changelog.md) ## Endpoints - POST /v1/payment_intents: Create a payment intent (server-only; returns client_secret) - GET /v1/payment_intents/{client_secret}: Retrieve a payment intent (the secret, not the pi_ id) - POST /v1/payment_intents/{client_secret}/confirm: Confirm a payment intent (browser) - POST /v1/charges: Create a charge - GET /v1/charges: List charges - GET /v1/charges/{id}: Retrieve a charge - POST /v1/charges/{id}/capture: Capture an authorized charge - POST /v1/charges/{id}/void: Void (cancel) an authorized charge - POST /v1/refunds: Create a refund - GET /v1/refunds: List refunds - GET /v1/refunds/{id}: Retrieve a refund - POST /v1/plans: Create a plan (server-only `plans:write`) - GET /v1/plans: List plans - GET /v1/plans/{id}: Retrieve a plan - POST /v1/subscriptions: Create a subscription (`plan` is required) - GET /v1/subscriptions: List subscriptions - GET /v1/subscriptions/{id}: Retrieve a subscription - PATCH /v1/subscriptions/{id}: Change plan, pause, resume, or schedule a cancellation. Exactly one of plan / paused / cancel_at_period_end per call - DELETE /v1/subscriptions/{id}: Cancel a subscription - POST /v1/test/subscriptions/{id}/bill: Run this subscription's renewal now (TEST keys only) - GET /v1/invoices: List invoices (PAID row on auto-charge renewal; opening create does not mint) - GET /v1/invoices/{id}: Retrieve an invoice - POST /v1/invoices/{id}/send: Email an OPEN invoice (server-only `invoices:write`) - GET /v1/stores: List storefronts - GET /v1/stores/stats: Storefront counters + revenue - GET /v1/stores/{id}: Retrieve a storefront - GET /v1/metrics: KPI roll-up (gross / approved / net, rates, counts) - GET /v1/checkout-links: List hosted checkout links - GET /v1/checkout-links/{id}: Retrieve a checkout link - POST /v1/customer_sessions: Create a customer session (saved cards for Payment Element) - GET /v1/customer_sessions/payment_methods: List session customer's saved cards - POST /v1/checkout-links: Create a hosted checkout link - GET /v1/products: List products - POST /v1/products/{id}/tax_rates: Attach a tax rate to a product (server-only `tax:write`) - DELETE /v1/products/{id}/tax_rates/{tax_rate}: Detach a tax rate from a product - POST /v1/tax_rates: Create a tax rate (percent, e.g. 8.25; server-only `tax:write`) - GET /v1/tax_rates: List tax rates - GET /v1/tax_rates/{id}: Retrieve a tax rate - POST /v1/tax_rates/{id}: Update a tax rate (only the fields sent change) - DELETE /v1/tax_rates/{id}: Delete a tax rate (soft delete) - POST /v1/tax/calculate: Per-line tax for a cart from your own rates (`tax:read`, writes nothing) - GET /v1/wallet-config: Get wallet (Apple/Google Pay) config - GET /v1/wp-config: Get WorthyPayments card public key (wp: tokenization) - GET /v1/evervault-config: Get Evervault browser config - GET /v1/kount-config: Get Kount device-data collection config - GET /v1/element-frameable: Check whether an origin may embed the Payment Element - POST /v1/test/tokens: Mint test card tokens ({ number?, cvc? }; TEST only) - GET /v1/ping: Verify a key - POST /v1/check: Pre-checkout fraud evaluation (advisory) - POST /v1/check/recheck: Re-run a fraud check - POST /v1/report: Report a fraud signal - GET /v1/blocklist: List blocklist entries - GET /v1/3ds/config: Get 3-D Secure browser config - POST /v1/3ds/sessions: Create a 3-D Secure session - GET /v1/3ds/sessions/{id}: Retrieve a 3-D Secure session - GET /v1/3ds/sessions/{id}/status: Browser-safe 3DS session status - POST /v1/3ds/retry-decline: Retry a soft decline after 3DS - POST /v1/customers: Create a customer - GET /v1/customers: List customers - GET /v1/customers/{id}: Retrieve a customer - GET /v1/customers/{id}/state: Customer + active subscriptions + saved cards in one call (the entitlement answer) - GET /v1/customers/external/{externalId}/state: The same, keyed by YOUR customer id — no mapping table - POST /v1/customers/{id}: Update a customer - POST /v1/customers/{id}/payment_methods: Save a card to a customer - GET /v1/customers/{id}/payment_methods: List a customer's saved cards - DELETE /v1/customers/{customerId}/payment_methods/{pmId}: Delete a saved card (scoped) - DELETE /v1/customers/payment_methods/{pmId}: Delete a saved card - GET /v1/events: List events - GET /v1/events/{id}: Retrieve an event - POST /v1/webhook-endpoints: Register a webhook endpoint - GET /v1/webhook-endpoints: List webhook endpoints - GET /v1/webhook-endpoints/{id}: Retrieve a webhook endpoint - DELETE /v1/webhook-endpoints/{id}: Delete a webhook endpoint - POST /v1/webhook-endpoints/{id}/roll-secret: Roll the signing secret ## Machine-readable - [OpenAPI spec](/developers/api-reference/openapi.yaml) - [Full docs for LLMs](/developers/llms-full.txt)