Developers

Halba API

One API call for card payments: fraud screening, 3D Secure or an SCA exemption, authorization on your PSP and automatic fallback to a backup PSP.

Overview

You send the payment to Halba; Halba screens it with your fraud vendor, decides between 3D Secure, an SCA exemption or a direct authorization, sends it to the PSP chosen for you and, if that PSP fails for technical reasons, retries once on your backup PSP. You get one response with the outcome, the PSP that processed it and who bears the fraud liability.

Your PSPs are your own accounts: connect them in the dashboard (Routing & risk > Connections). The first PSP you connect is the primary, the second is the backup.

Environments and keys

EnvironmentDashboardAPIKeys
Sandboxsandbox.halba.aihttps://api.sandbox.halba.aihb_test_…
Productionapp.halba.ai (coming soon)https://api.halba.aihb_live_…

The sandbox accepts only test credentials for your PSPs and never moves real money. Create keys under Developers > API keys. The secret key is shown once: keep it on your server. Each key belongs to one workspace. To rotate, create a new key, deploy it, then revoke the old one: a revoked key stops working within a minute.

Authorization: Bearer hb_test_…

halba.js in the browser

Fraud screening and 3D Secure need data that only the browser has: IP address, browser headers, screen and time zone, and a fraud session. halba.js collects them on your checkout page with the publishable key (hb_pk_test_…), which can only collect device data and complete 3DS for your payments.

<script src="https://api.sandbox.halba.ai/v1/halba.js"></script>
<script>
  const halba = Halba("hb_pk_test_…");            // publishable key
  const { clientSession } = await halba.collect(); // send it to your server with the order

  // after your server created the payment:
  if (payment.next_action) payment = await halba.handleNextAction(payment);
</script>

collect() returns a clientSession id, valid for one hour. halba.js uses no cookies and does not read your page.

Your first payment

From your server, with the secret key. The card is a token from the vault: in the sandbox, your connected Stripe test account, so Stripe test tokens such as tok_visa work. With client_session you can leave out device.

curl https://api.sandbox.halba.ai/v1/payments \
  -H "Authorization: Bearer hb_test_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_1001" \
  -d '{
    "merchant_reference": "order_1001",
    "amount": { "value": 12990, "currency": "EUR" },
    "payment_method": { "type": "card", "card": { "token": { "vault": "stripe", "id": "tok_visa" } } },
    "customer": { "email": "maria.bianchi@example.com" },
    "client_session": "cs_…"
  }'

This is the minimum. Addresses, order items and account history are optional but improve fraud screening and exemptions; if your fraud vendor requires one of them, the response is 422 with the list of missing fields and nothing is charged.

{
  "id": "att_5229a8c1-a2d7-4216-8781-bec068a9afd3",
  "object": "payment",
  "status": "authorized",
  "psp": "stripe",
  "psp_reference": "pi_3UYun1CxTwW6t0Np0xFVJQtn",
  "fell_back": false,
  "next_action": null,
  "decline": null,
  "authentication": { "outcome": "authenticated", "eci": "05", "challenged": false, "exemption": null },
  "liability": { "holder": "issuer", "basis": "3ds_authenticated" },
  "amounts": { "authorized": 12990, "captured": 0, "capturable": 12990, "refunded": 0, "refundable": 0, "voided": false }
}

Payment outcomes

POST /v1/payments answers 201 whenever a payment was created, declines included: check status.

statusMeaning
authorizedApproved by the issuer or provider; capture it when you ship.
capturedApproved and already collected (iDEAL, EPS, Trustly, TWINT).
declinedRefused by the issuer or the PSP; decline.code says why.
blockedStopped before the PSP, by the fraud screening or a failed 3D Secure.
requires_fingerprint, requires_challenge3D Secure needs the customer's browser.
requires_actionSend the customer to next_action.url to approve at PayPal, Klarna or their bank.
processing, canceled, expiredRedirect methods: waiting for the provider, given up by the customer, not completed in time.

fell_back: true means the backup PSP processed the payment. liability tells you who bears a fraud chargeback.

3D Secure

When the issuer asks for it, the payment pauses with requires_challenge (or requires_fingerprint). With halba.js, pass the payment to halba.handleNextAction(payment): it shows the bank's page above your checkout and resolves with the final payment. Without halba.js, open next_action.url and then call POST /v1/payments/{id}/resume from your server.

Response details

The base response is small: status, amounts, PSP, decline reason and your metadata. Ask for more with expand, in the query of a GET or in the body of a POST.

curl "https://api.sandbox.halba.ai/v1/payments/att_…?expand=fraud,liability" \
  -H "Authorization: Bearer hb_test_…"

{
  "id": "att_…", "status": "authorized", …,
  "fraud": { "vendor": "riskified", "verdict": "approve", "guaranteed": true, … },
  "liability": { "holder": "issuer", "basis": "3ds_authenticated" }
}
BlockWhat you get
authentication3DS outcome, ECI, challenge, exemption requested.
fraudVendor, verdict, SCA advice, guarantee, reason.
liabilityWho bears a fraud chargeback, and why.
routingPrimary and final PSP, every attempt with its outcome.
cardBrand, BIN, last 4, expiry, funding, issuer country and name, PAR.
operationsCaptures, voids and refunds with their status.
timelineEvery step of the flow, for debugging.

PayPal, Klarna, iDEAL and other redirect methods

Same call, different payment_method.type, plus a return_url. Halba answers requires_action with a link on pay.halba.ai: send the customer there. Halba forwards them to the provider, checks the result with the provider server-side and sends them back to your return_url.

curl https://api.sandbox.halba.ai/v1/payments \
  -H "Authorization: Bearer hb_test_…" -H "Content-Type: application/json" \
  -d '{ "merchant_reference": "order_1001", "amount": { "value": 1000, "currency": "EUR" },
        "payment_method": { "type": "ideal" },
        "return_url": "https://shop.example.com/checkout/return" }'

{
  "id": "att_…", "status": "requires_action",
  "next_action": { "type": "redirect", "url": "https://sandbox.pay.halba.ai/r/att_…?t=…" }
}

# back on your return_url:
# https://shop.example.com/checkout/return?halba_payment_id=att_…&halba_status=captured
  • Methods: paypal, klarna, klarna_paynow, ideal, eps, trustly, twint, through your Adyen connection with the method active.
  • Outcome: PayPal and Klarna end authorized (capture when you ship); iDEAL, EPS, Trustly and TWINT end captured. Also processing, declined, canceled or expired after one hour.
  • Confirm on your server with GET /v1/payments/{id}: the query parameters on your return page only tell you which payment it is.
  • With halba.js, halba.handleNextAction(payment) does the redirect for you.

Capture, void and refund

Point to the payment with Halba's id or your merchant_reference.

# capture what is left (or pass "amount" for a partial capture)
curl -X POST https://api.sandbox.halba.ai/v1/payments/order_1001/capture \
  -H "Authorization: Bearer hb_test_…" -H "Idempotency-Key: capture-order_1001"

# refund part of it
curl https://api.sandbox.halba.ai/v1/payments/order_1001/refunds \
  -H "Authorization: Bearer hb_test_…" -H "Content-Type: application/json" \
  -d '{ "amount": 2000, "reason": "Item returned" }'
  • Capture: without amount, everything left. A partial capture releases the rest, unless multiple captures are enabled on the PSP connection (then pass final: false).
  • Void: cancels the whole authorization, only before any capture.
  • Refund: all or part of the captured amount, several times, up to the captured total. With Policy Protect, pass the customer's request as claim.

Each call returns an operation. Some PSPs confirm later: the operation is pending, then succeeded or failed.

Login protection

Call Halba on every login attempt with the client_session from halba.js on your login page. With Riskified Account Secure the answer is allow, notify or challenge; without a vendor, always allow with screened: false.

curl https://api.sandbox.halba.ai/v1/events/login \
  -H "Authorization: Bearer hb_test_…" -H "Content-Type: application/json" \
  -d '{ "customer": { "id": "cus_42", "email": "maria@example.com" }, "status": "success", "client_session": "cs_…" }'

{ "id": "lg_…", "object": "login_event", "decision": "allow", "screened": true, "vendor": "riskified" }

Idempotency

Send an Idempotency-Key with every payment and operation. Retrying with the same key and body within 24 hours returns the original response with Idempotent-Replayed: true. Same key with a different body: 422; still running: 409; after a 5xx you can retry with the same key.

Errors and limits

{
  "error": {
    "type": "invalid_request",
    "message": "…",
    "issues": [{ "path": "customer.email", "message": "…" }]
  }
}
HTTPtypeWhen
400invalid_requestBody is not JSON or the Idempotency-Key is malformed.
401authentication_errorMissing, unknown or revoked key.
404not_foundPayment not found in this workspace and environment.
409conflictRequest in progress, nothing left to capture, or 3DS event not expected.
422invalid_requestInvalid or missing data, or amount above what is available.
422configuration_errorNo PSP connected, or multiple captures not enabled.
429rate_limitedToo many requests with invalid keys from your IP.
502provider_errorA PSP or vendor did not respond: retry with the same Idempotency-Key.

Sandbox test cards

TokenResult
tok_visa, tok_mastercardAuthorized
tok_threeDSecure2Required3D Secure challenge, then authorized
tok_chargeDeclinedInsufficientFundsDeclined: insufficient funds
tok_chargeDeclinedProcessingErrorTechnical failure on the primary PSP, authorized on the backup