# 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`.