AgentCard
Pay for a booking with a card the end user keeps in their AgentCard vault, approved with their passkey.
Where it works
AgentCard works on Resy and OpenTable, on v1, with every v1 key, live or test.
When a venue cannot be paid through AgentCard, creating the booking returns HTTP 409 vault_unavailable and nothing is created; offer the end user a different venue or time.
Before you start
You need all of these before your first booking:
- An Ophelia v1 API key.
- An AgentCard organization token. Exchange your organization's
client_idandclient_secretfor a client-credentials token (AgentCard: Create an access token). AgentCard API keys are rejected. - An AgentCard user with a stored card. The user must be connected to your AgentCard organization (AgentCard: Adding a card, Check for stored cards).
- The end user's real details. Name, email, and a mobile number they can read right now: the venue texts a code to it.
- A way to reach the end user within minutes, for the SMS code and the approval link.
Integrate
Every sample is marked Ophelia or AgentCard by the service it calls.
1.Search availability
Pass check_card_required: true to see whether the venue needs a card. If a slot needs no card, the booking may confirm without ever pausing for payment, so handle confirmed at any step.
2.Create the booking
Send "payment_collection": "agentcard_external" and no card fields. The optional metadata.billing_postal_code, billing_address_line1, billing_city, billing_state and billing_country are the billing details the processor's address check sees, so send the end user's real ones; without them the check sees postal code 94103 and country US. end_user is optional. Spell the mode exactly agentcard_external: an unrecognized value is not rejected, it is treated as a booking with no card and fails at the venue's card step.
The call can wait up to 150 seconds for the booking to move. It returns HTTP 202 while the booking is still processing, and HTTP 200 otherwise. A 200 means "not processing", never "confirmed". Always read status. The booking often comes back as processing; read it every few seconds until it changes. On Resy it reads requires_action for the SMS code within about a minute:
The order differs by provider: on Resy the SMS code comes first, on OpenTable the payment approval comes first. The code step can also be skipped. Branch on next_action.type every time.
3.Relay the SMS code
When next_action.type is otp, send the code the end user received. Relay it immediately and treat next_action.expires_at as the upper bound. It is currently 5 minutes from the pause, but never hard-code it; always read expires_at.
4.Create the AgentCard authorization
When next_action.type is payment_authorization, the booking is paused at the venue's card step. On Resy this comes about a minute after the SMS code. next_action.message sums up what to do: send agentcard_request with your user to AgentCard, get the end user's approval, and send the result back before expires_at.
merchantcheckout_originpspmoderequest.urlrequest.methodrequest.headersrequest.bodyamount, currencyuser (you add)Copy every key unchanged, add user, and POST it to AgentCard's /api/v2/checkout/authorizations with your organization token. Deliver the returned approvalUrl to the end user (AgentCard: Choose a platform).
5.Wait for approval, then hand back
Read the AgentCard authorization until its status is approved. awaiting_approval and submitted_on_device mean keep waiting; declined, expired and cancelled mean abandon (step 6). Then send its response object to Ophelia unchanged as {"psp_response": {"status", "headers", "body"}}. Send only response, not the whole authorization object.
This call also waits up to 150 seconds, so it usually returns confirmed, with the venue's confirmation code, or failed. On Resy that takes about ten seconds. If it returns 202, read GET /v1/bookings/{booking_id} until the booking is confirmed or failed. If psp_response is malformed, you get HTTP 400 and the booking stays paused; fix the body and retry before expires_at. An answer after expires_at also gets HTTP 400, but the booking is failed: payment_outcome_unknown for a late psp_response (the card may have reached the venue), payment_approval_expired for a late psp_abort. A 400 carries only an error message, so read GET /v1/bookings/{booking_id} to tell the two apart.
expires_at. When Ophelia's expires_at passes, stop polling, cancel the AgentCard authorization (AgentCard: Cancel a checkout authorization) so the end user cannot approve a payment that can no longer complete, and tell them to start again. Leave a margin of about 30 seconds before expires_at for your own /continue call.6.Abandon a payment
If the end user declines, or the AgentCard authorization expires or is cancelled, abandon the payment. decline_code is an optional reason for your own records; it is not returned on the booking. The booking fails with error_code: "payment_declined".
Cancelling the Ophelia booking does not cancel the AgentCard authorization. Cancel it on AgentCard yourself.
Webhook-driven variant
You don't have to poll Ophelia. The booking.requires_action webhook carries the full next_action, including agentcard_request, and booking.confirmed, booking.failed or booking.cancelled tells you how it ended. The webhook's booking is a summary (id, status, provider, error_code, error_message, next_action) without the confirmation code, so read GET /v1/bookings/{booking_id} after booking.confirmed. Each delivery is signed in the Ophelia-Signature header. You can also have AgentCard notify you instead of polling it (AgentCard: checkout authorization events).
Full example
One server-side function that runs the whole loop: create, relay the SMS code through getOtp(), deliver the approval link through sendApprovalLink(url), wait on AgentCard, continue, and handle every terminal status. It enforces expires_at locally.
Timing
otp action's next_action.expires_at. Currently 5 minutes from the pause.next_action.expires_at.An answer that arrives after expires_at will not complete the reservation. Start a new booking.
Errors
vault_unavailable: this venue cannot be paid through AgentCard.agentcard_disabled: AgentCard payments are off.response object, fix the body, and retry before expires_at.error message. The booking is failed: payment_outcome_unknown after a late psp_response, payment_approval_expired after a late psp_abort.payment_declined: you abandoned the payment.payment_approval_expired: no answer before expires_at.payment_outcome_unknown: Ophelia cannot tell whether the payment step finished.payment_setup_failed: Ophelia could not prepare the payment for this venue.payment_replay_failed: the processor's answer could not be applied to the checkout.card_declined: the processor declined the end user's card.See Errors for every code.
Testing
Ophelia has no sandbox for AgentCard payments. Every booking is a real reservation, including on test keys, which use the 3-booking free allowance. AgentCard sandbox cards cannot pay a live venue, so an end-to-end test needs AgentCard production credentials and a real card. The whole loop takes about six minutes:
- Pick a slot. Search availability on a Resy or OpenTable venue that holds a card, a few days out, with
party_size: 2. The response sayscard_required: true. - Book. Create the booking with that slot, your real name, email and a phone you can read, and
"payment_collection": "agentcard_external". Send no card fields. - Relay the code. When the booking reads
next_action.type: "otp", post the code you received as{"otp_code": "..."}. - Authorize. When it reads
next_action.type: "payment_authorization", addusertonext_action.agentcard_request, create the AgentCard authorization, open itsapprovalUrl, and approve. - Hand it back. Read the authorization until
approved, then post itsresponseaspsp_response. The booking readsconfirmedwith the venue's confirmation code. - Cancel. Call
POST /v1/bookings/{booking_id}/cancelright away. It is a real reservation, and the venue's no-show terms apply.
Approve within next_action.expires_at, 15 minutes from the payment pause. To rehearse only the AgentCard half (vault, authorization, approval), use AgentCard's sandbox and shop.agentcard.sh.
Security
The card number and CVV never reach Ophelia or your servers. Your AgentCard credentials never reach Ophelia. Ophelia sees the processor's response and the billing details you send. The billing details are stored with the booking; the processor's response is stored with the booking attempt, encrypted.
Your server holds:
- Your AgentCard organization's
client_id,client_secretand org token. Keep them server side. - The AgentCard
user_idof each end user.
Next steps
- AgentCard: platforms, for where to show approval links.
- AgentCard: checkout_authorization.* webhooks
- Endpoint reference and Booking lifecycle
- Errors