Accept a Card Payment
This guide walks through taking a one-time card payment from start to finish: verify the cardholder with 3D Secure, create the sale and confirm the result. It is the recommended starting point for a new integration.
Before you start
- You need an API key, provisioned by your SoftLemon admin team and sent on every request as
Authorization: Bearer {YOUR_API_KEY}. - All requests in this guide run against the sandbox at
https://api.sandbox.softlemons.com. - You need a return URL on your site for the 3D Secure redirect (the
auth_urlbelow). - Amounts in requests are in major units, so
12.50means EUR 12.50. Responses and webhooks report amounts in minor units, so the same value comes back as1250. - Use the sandbox card numbers from the 3D Secure test cards section of the Merchant API reference to exercise each outcome.
Step 1: Verify the cardholder with 3D Secure
Start with POST /api/v1/3ds/verify because card payments must be authenticated before they are charged. SoftLemon runs a server-managed 3DS flow, so you never handle CAVV, ECI or DS Transaction IDs yourself. The gateway stores them and attaches them to the payment later.
With the test card 4200000000000091 the issuer does not require a challenge and the response returns immediately:
JSON
status: full_auth means authentication succeeded. Keep data.public_id (tds_01k2h4x9m3n5p7q9r1s3t5v7w9 here). It is the verification's identifier and you will pass it when creating the sale in Step 3. Treat it as an opaque string. The numeric id next to it is deprecated and will leave the response on a date announced in the changelog; see conventions.
The immediate response is not always a full authentication. Read status, not just the HTTP code:
status | Meaning | What to do |
|---|---|---|
full_auth | The issuer authenticated the cardholder. | Continue to Step 3. |
attempt | The issuer could not fully authenticate the cardholder but returned an attempt proof (ECI 06 or 01). | Continue to Step 3. The attempt proof is forwarded with the payment. |
unavailable | The card is not enrolled for 3DS or authentication could not be performed. No authentication data exists and the message says so. | You may still create the sale. It is processed without 3DS and can be soft-declined with ERR_3DS_REQUIRED where SCA is mandatory. |
failed | The issuer refused the authentication. Returned as HTTP 400 with code ERR_3DS_FAILED. | Do not create the sale. Ask for another card. |
Step 2: Handle the challenge when the issuer requires one
Some cards trigger a bank challenge instead (test with 4200000000000042). In that case the response carries a challenge_url and no final status yet:
- Redirect the cardholder to
challenge_url. The bank runs its own verification there (a code, an app approval or similar). - When the cardholder finishes, the gateway redirects them back to your
auth_urlwith query parameters:?card_verification_id={public_id}&status={outcome}, wherecard_verification_idis the verification'stds_...public id and the outcome is one of the statuses in the table above (full_auth,attempt,unavailableorfailed). - On
full_authorattempt, use thecard_verification_idvalue exactly like thepublic_idfrom the frictionless case, passed through unchanged. Onunavailable, decide whether to charge without 3DS. Onfailed, show the customer a payment failure and let them try another card.
Your return page should handle every outcome. Nothing has been charged at this point in either flow, and a sale that references a failed or unfinished verification is refused with HTTP 422.
Step 3: Create the sale
Now charge the card with POST /api/v1/transactions. Three fields matter beyond the card details:
transaction_type: "sale"charges immediately with no separate capture step. (Useauthinstead when you want to reserve funds first. See Authorize now, capture later.)card_verification_data.idlinks the 3DS verification from Step 1 or 2: thepublic_idfrom the verify response or thecard_verification_idfrom the redirect. The stored CAVV, ECI and DS Transaction ID are attached automatically.referenceis your own id for this payment. The gateway allows only one active payment per reference, which protects your customer from double charges. See the duplicate protection guide.
A successful response returns the transaction:
JSON
Store data.id. It is the gateway transaction id you will use for refunds, voids and status checks. See the transaction statuses guide for what each status value means.
Step 4: Confirm the result
Treat webhooks as your primary confirmation. When the sale completes, your registered endpoint receives a signed transaction.succeeded event within seconds. Verify the signature, acknowledge with a 2xx response and update your order. The webhook integration guide covers registration, verification and retries.
For an on-demand answer (for example after downtime or during reconciliation), poll the status endpoint:
Errors you should handle
| Response | Meaning | What to do |
|---|---|---|
401 | Missing or invalid API key. | Check the Authorization header. |
409 with ERR_DUPLICATE | The reference already has an active payment. data.transaction_id is the original. | Treat the payment as already made. Do not retry with the same reference. |
422 | Validation failed. The response lists the field errors. | Fix the request. Nothing was charged. |
400 with ERR_DO_NOT_RETRY | The same declined card was retried within the cooldown window. | Wait or ask the customer for another card. |
400 with ERR_3DS_REQUIRED | The sale was sent without card_verification_data and the issuer requires authentication. The transaction is recorded as failed with status_reason 3ds_required; data.transaction is that row and data.next_action points at POST /api/v1/3ds/verify. | Go back to Step 1, run the verification, then create a new sale with card_verification_data.id. The refused sale cannot be continued. |
422 with ERR_3DS_REQUIRED or ERR_3DS_FAILED | The card_verification_data.id you sent belongs to a verification that has not finished, or that ended in failed. | Wait for the outcome, or run a new verification. Nothing was charged. |