Payment Methods

A Payment Method is a saved card or bank account you can charge again. Cards are never sent to this API as numbers: the browser tokenizes the card with hlhunt.js (or the capture-session flow below) and your server stores the token. Bank accounts are sent as routing and account numbers to this endpoint from your server. Both kinds can be linked to a customer with customer_id. The legacy shape with type "PAYMENT_CARD" and raw card numbers that older pages showed is rejected with 400 pci_vault_required.

POST
/v1/capture-session

Create a card capture session (browser)

Opens a short-lived capture endpoint in HL Hunt's PCI vault. This is what hlhunt.js calls for you; call it directly only if you are building your own browser or mobile tokenization. Publishable key (pk_live_ / pk_test_). CORS is open on this route.

Request body

FieldTypeRequiredDescription
ttlstringoptionalLifetime such as "5m", "10m", "1h". Default 10m, capped at 60m. Invalid formats are rejected with 400 invalid_param.
referencestringoptionalYour own reference, stored on the capture.

Response

FieldTypeDescription
objectstring"capture_session".
capture_urlstringHTTPS URL the browser POSTs the card (or bank) fields to.
capture_secretstringSend as the X-PCIVault-Capture-Secret header on that POST.
capture_idstringId of the capture endpoint.
expires_atstringISO 8601 expiry.
key_userstringVault key user; not needed for the normal flow.
livemodebooleanMode of the key that created the session.
  • HTTP 201. The browser then POSTs JSON { card_number, card_holder, cvv, expiry_month ("MM"), expiry_year ("YYYY"), expiry_year_short } — or { routing_number, account_number, account_type, account_holder } for a bank account — to capture_url and receives { token, reference }. Use token as source on POST /v1/charges (one-time) or as payment_token on POST /v1/payment-methods (save). See the hlhunt.js guide.
  • 403 merchant_not_onboarded · 502 vault_error when the vault is unavailable.
  • Legacy alternative: POST https://payments.hlhunt.org/api/tokenize/session with a secret key returns { tokenizeUrl, tokenSecret, tokenSecretHeader: "X-Tokenize-Secret", expiresAt }; the browser POSTs the same card fields to tokenizeUrl with that header and gets { token, reference }. It still works but hlhunt.js and /v1/capture-session are the supported path.
curl -X POST https://api.hlhunt.org/v1/capture-session \
  -H "Authorization: Bearer pk_live_your_publishable_key" \
  -H "Content-Type: application/json" \
  -d '{ "ttl": "10m" }'
POST
/v1/payment-methods

Save a card (from a token)

Store a tokenized card for later charges. Accepts a secret or a publishable key. The token comes from hlhunt.js createToken or the capture-session flow; card.* fields are display data only and are stored as given.

Request body

FieldTypeRequiredDescription
typestringrequired"card".
payment_tokenstringrequiredThe token returned by the browser capture. Legacy alias: vault_token.
token_referencestringoptionalThe reference returned next to the token. Legacy alias: vault_reference.
cardobjectoptional{ last4, brand, exp_month, exp_year } for display. hlhunt.js returns these in result.card. Sending card.number or card.cvc is rejected with 400 pci_vault_required.
namestringoptionalCardholder name. Default "Card".
customer_idstringoptionalLink the card to a customer of this business. Also triggers payment_method.saved.
merchant_idstringoptionalMulti-business accounts only.

Response

FieldTypeDescription
idstringPayment method id: pcivault_<token> (pcivault_test_<token> for test keys). Use it as source on POST /v1/charges.
objectstring"payment_method".
typestring"card".
cardobject{ brand, last4, exp_month, exp_year } — exactly the display values you sent, or null.
namestringCardholder name.
createdstringISO 8601.
  • HTTP 201. 400 missing_required_param (type, payment_token) · 400 pci_vault_required (raw card data) · 400 invalid_type · 403 merchant_not_onboarded.
  • The same card tokenizes to the same token, so saving it twice updates the existing record instead of creating a second one.
  • Emits payment_method.created, and payment_method.saved when customer_id is set.
curl -X POST https://api.hlhunt.org/v1/payment-methods \
  -H "Authorization: Bearer sk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "card",
    "payment_token": "9f1c3b…token from hlhunt.js…",
    "token_reference": "ref_from_hlhunt_js",
    "name": "Jenny Rosen",
    "customer_id": "c1m9k2x7e0001qz8f4h2b6n3p",
    "card": { "last4": "4242", "brand": "visa", "exp_month": 12, "exp_year": 2029 }
  }'
POST
/v1/payment-methods

Save a bank account (ACH)

Store a US bank account for ACH debits. Routing and account numbers are sent from your server to this endpoint. Since 2026-10-19 a bank account entered this way must be verified by a $0.01 microdeposit before its first debit: the response comes back with status requires_verification and the account owner submits the FNX code from their bank statement to POST /v1/payment-methods/{id}/verify.

Request body

FieldTypeRequiredDescription
typestringrequired"bank_account".
bank_account.routing_numberstringrequired9-digit ABA routing number.
bank_account.account_numberstringrequiredAccount number.
bank_account.account_typestringoptional"checking" or "savings". Default checking.
namestringoptionalAccount holder name. Default "Bank Account". There is no separate bank-name field; the holder is what is stored and shown.
customer_idstringoptionalLink the account to a customer. The instrument is then created on the buyer's own identity, never the merchant's.
merchant_idstringoptionalMulti-business accounts only.

Response

FieldTypeDescription
idstringProcessor instrument id (e.g. PI…). Use it as source on POST /v1/charges and as {id} on …/verify.
objectstring"payment_method".
typestring"bank_account".
bank_accountobject{ last4, account_type }.
namestringAccount holder name.
statusstringrequires_verification until the microdeposit code is accepted, then verified.
verificationobjectPresent while requires_verification: { method: "microdeposit", message } explaining the $0.01 deposit and the FNX code.
createdstringISO 8601.
  • HTTP 201. 400 missing_required_param (routing or account number) · 400 bank_account_error when the processor rejects the account · 403 merchant_not_onboarded.
  • Charging an unverified account answers 402 card_error with code bank_account_unverified; a permanently failed verification answers bank_account_verification_failed. Three code attempts per deposit, 30-day window.
  • Emits payment_method.created.
curl -X POST https://api.hlhunt.org/v1/payment-methods \
  -H "Authorization: Bearer sk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "bank_account",
    "name": "Jenny Rosen",
    "customer_id": "c1m9k2x7e0001qz8f4h2b6n3p",
    "bank_account": {
      "routing_number": "021000021",
      "account_number": "9876543210",
      "account_type": "checking"
    }
  }'
POST
/v1/payment-methods/{payment_method_id}/verify

Verify a bank account (microdeposit code)

Submit the code printed in the statement description of the $0.01 deposit (it starts with FNX). On success the bank account becomes chargeable.

Request body

FieldTypeRequiredDescription
payment_method_idstring (path)requiredThe bank account's id from POST /v1/payment-methods.
verification_codestringrequiredThe FNX code, e.g. FNX7Q2M. Case and spaces are normalised.

Response

FieldTypeDescription
id / object / typeThe payment method.
statusstring"verified".
bank_accountobject{ last4 }.
  • 400 missing_required_param · 400 not_a_bank_account · 404 resource_missing.
  • 422 { error: { type: "invalid_request_error", code, message, attempts_remaining, terminal } } when the code is wrong or the deposit can no longer be verified. code is the processor's reason in lower case: incorrect_microdeposit_verification_code (retry, attempts_remaining tells you how many), microdeposit_verification_attempts_exceeded, microdeposit_verification_expired, microdeposit_returned, total_microdeposit_attempts_exceeded_account_locked, invalid_bank_account_routing_number (terminal: true — collect a different bank account), invalid_field (code not in FNX… form).
curl -X POST https://api.hlhunt.org/v1/payment-methods/PIxxxxxxxxxxxxxxxxxx/verify \
  -H "Authorization: Bearer sk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "verification_code": "FNX7Q2M" }'
GET
/v1/payment-methods

List Payment Methods

Without customer_id, the payment methods the processor holds for the key's business. With customer_id, the instruments linked to that customer (enabled cards and bank accounts, plus bank accounts still awaiting verification).

Parameters

FieldTypeRequiredDescription
customer_idstring (query)optionalA customer id of this business. An unknown customer returns an empty list.

Response

FieldTypeDescription
objectstring"list".
data[].idstringPayment method id (source for /v1/charges).
data[].objectstring"payment_method".
data[].typestring"card" or "bank_account".
data[].statusstringBank accounts in the customer_id list only: requires_verification or verified.
data[].cardobjectCards: { brand, last4, exp_month, exp_year }. Absent for bank accounts.
data[].bank_accountobjectBank accounts: { last4, account_type }. Absent for cards.
data[].namestring | nullCardholder or account holder.
data[].createdstring | nullISO 8601.
has_morebooleanAlways false (no pagination on this endpoint).
curl -X GET "https://api.hlhunt.org/v1/payment-methods?customer_id=c1m9k2x7e0001qz8f4h2b6n3p" \
  -H "Authorization: Bearer sk_live_your_api_key"
GET
/v1/customers/{customer_id}/payment-methods

List a Customer's Payment Methods

Lists the enabled instruments linked to a customer. This endpoint returns HL Hunt's internal instrument id as id and the chargeable processor-level id as processor_instrument_id (both are accepted as source on POST /v1/charges). Bank accounts still awaiting microdeposit verification are not listed here; GET /v1/payment-methods?customer_id=… includes them with their status.

Parameters

FieldTypeRequiredDescription
customer_idstring (path)requiredThe customer's id. 404 not_found when it belongs to another business.

Response

FieldTypeDescription
objectstring"list".
data[].idstringInternal instrument id (also accepted as source on /v1/charges).
data[].processor_instrument_idstring | nullThe processor-level id — the same value GET /v1/payment-methods returns as id.
data[].typestring"card" or "bank_account".
data[].cardobjectCards: { brand, last4, exp_month, exp_year }. Absent for bank accounts.
data[].bank_accountobjectBank accounts: { bank_name (the account holder name), last4, account_type }. Absent for cards.
data[].name / createdHolder name and ISO 8601 creation time.
total / has_moreCount of rows; has_more is always false.
curl -X GET https://api.hlhunt.org/v1/customers/c1m9k2x7e0001qz8f4h2b6n3p/payment-methods \
  -H "Authorization: Bearer sk_live_your_api_key"