# 3DS with Hosted Fields Building with an AI agent? Start at https://docs.payme.io/agents.md When 3D Secure is active on the seller's account and a sale meets its selection rules (a foreign card, an amount above the account's threshold, or 3 or more installments), pay the Hosted Fields token with `pay-sale` and the buyer's browser data. PayMe then runs 3D Secure: either frictionless, or with a challenge screen the buyer completes. No `services` parameter is needed to turn it on. **Always send the browser data (`meta_data_jwt`).** Without it, `pay-sale` charges the token without 3D Secure. The challenge screen opens at the `redirect_url` PayMe returns. If you restrict hosts with allow-lists or a Content Security Policy, allow the host of that URL. See [Environments](/payments/getting-started/environments). ## Step 1: Tokenize Tokenize the card as in the [Hosted Fields guide's minimal integration](/accepting-payments/checkout-and-in-person/hosted-fields-jsapi-guide#minimal-integration). The result's `token` is the `buyer_key`. The 3D Secure part needs no extra HTML. ## Step 2: Collect the browser data In the same page, call `PayMe.clientData(testMode)` and send the `hash` it resolves to your server, together with the token: ```html ``` Pass the same `testMode` you use for `PayMe.create`: `PayMe.clientData(true)` in the sandbox, `PayMe.clientData(false)` (or no argument) in production. The data is encrypted for that environment. If your site sets a Content Security Policy, allow `connect-src https://hf.payme.io`. ## Step 3: Create the sale On your server, call [generate-sale](/payments/hosted-payment-page/post-generate-sale) **without** a `buyer_key`, and keep the `payme_sale_id` it returns. Set the two URLs here: `pay-sale` doesn't take them. - `sale_callback_url`: PayMe posts the result there, including the result of a challenge. - `sale_return_url`: where the buyer lands after a challenge. ```json { "seller_payme_id": "MPLDEMO-MPLDEMO-MPLDEMO-1234567", "sale_price": 1000, "currency": "ILS", "product_name": "Order 1234", "sale_payment_method": "credit-card", "sale_callback_url": "https://www.example.com/payment/callback", "sale_return_url": "https://www.example.com/payment/success" } ``` The response has other fields as well; you need `payme_sale_id`: ```json { "status_code": 0, "payme_sale_id": "SALE1655-3843870J-NTPYLJNW-SOWRLK9X" } ``` ## Step 4: Pay the sale On your server, call [pay-sale](/payments/direct-api/post-pay-sale) with the sale, the token and the hash: ```json { "payme_sale_id": "SALE1655-3843870J-NTPYLJNW-SOWRLK9X", "buyer_key": "BUYERDEMO-BUYERDEMO-BUYERDEMO-1234567", "meta_data_jwt": "" } ``` With a `buyer_key`, the buyer's name, email and ID come from the token, so don't send them. `installments` is read only when the sale lets the buyer choose the number of installments. `POST` it to `https://sandbox.payme.io/api/pay-sale`, or `https://live.payme.io/api/pay-sale` in production. ## Step 5: Handle the result | `status_code` | Meaning | What to do | |---|---|---| | `5` | A challenge is needed | Send the buyer's browser (the top window) to `redirect_url`. The result is posted to `sale_callback_url`, and the buyer lands on `sale_return_url`. | | `0` | Paid without a challenge: `sale_status` is `completed` | Confirm, then fulfil the order | | `1` | Declined: `sale_status` is `failed` | `status_error_code` and `status_error_details` say why | Don't read `payme_status`: it is `success` on a declined payment too. `status_message` and `status_error_details` are in Hebrew unless you add `"language": "en"` to the `pay-sale` request. A challenge: ```json { "status_code": 5, "status_message": "Generating validation page, please wait", "redirect_url": "" } ``` Paid without a challenge (other fields omitted): ```json { "status_code": 0, "payme_status": "success", "status_error_code": 0, "payme_sale_id": "SALE1655-3843870J-NTPYLJNW-SOWRLK9X", "payme_sale_code": 20511311, "sale_status": "completed", "currency": "ILS", "price": 1000, "payme_transaction_auth_number": "1172408", "buyer_card_mask": "490763******9022", "sale_3ds": true } ``` Declined (other fields omitted): ```json { "status_code": 1, "payme_status": "success", "status_error_code": 20004, "status_error_details": "Refusal by credit company. Please try a different card", "payme_sale_id": "SALE1655-3843870J-NTPYLJNW-SOWRLK9X", "sale_status": "failed", "currency": "ILS", "price": 1000 } ``` **Before you fulfil**, confirm the payment with the callback or with [get-transactions](/payments/query-apis/post-get-transactions). `sale_is_3ds` there tells you whether the payment was authenticated with 3D Secure. ## Test in the sandbox The sandbox applies the same selection rules as production, so use a card and amount that trigger 3D Secure. See [3D Secure in the sandbox](/payments/getting-started/test-cards-and-payment-methods#3d-secure-in-the-sandbox) for the test cards. ## Move to production In production, call `PayMe.create(publicKey, { testMode: false })` with the production public key and `PayMe.clientData(false)`, and send the server calls to `https://live.payme.io/api`.