One-Time Payments
Take a single card payment with your own form: tokenize the card in the browser with hlhunt.js, then charge the token from your server. If you would rather not build a form, use a Checkout Session.
How It Works
- The browser sends the card to HL Hunt's vault through hlhunt.js and gets a token. The number never reaches your server.
- Your server charges the token once with
POST /v1/charges, or saves it withPOST /v1/payment-methodsand charges the saved id. - You read the status from the response and finish the order on the
charge.succeededwebhook.
Raw card numbers are never accepted by the API: a request carrying card.number is rejected with 400 pci_vault_required. (An older version of this page showed type: "PAYMENT_CARD" with the number in the body; that shape has never been accepted by the current API.)
Step 1: Tokenize the card in the browser
Load hlhunt.js with your publishable key and call createToken:
<script src="https://payments.hlhunt.org/js/hlhunt.js"></script>
<script>
var hlhunt = HLHunt('pk_live_your_publishable_key');
hlhunt.createToken({
card: { number: '4242 4242 4242 4242', exp_month: 12, exp_year: 2029, cvc: '123', name: 'Jenny Rosen' }
}).then(function (result) {
if (result.error) { showError(result.error.message); return; }
// result.token, result.reference, result.card = { last4, exp_month, exp_year, brand }
postToYourServer({ token: result.token, reference: result.reference, card: result.card });
});
</script>The promise resolves with token, reference and a card display object, or with an error. Use createBankToken for a bank account.
Step 2: Charge the token, or save it
From your server, with your secret key. To charge once:
POST /v1/charges
Authorization: Bearer sk_live_your_secret_key
Idempotency-Key: order-1234-attempt-1
{
"amount": 5000,
"currency": "USD",
"source": "<result.token>",
"vault_reference": "<result.reference>",
"description": "Order #1234",
"customer_email": "jenny@example.com",
"metadata": { "order_id": "1234" }
}To keep the card for later charges, save it first (optionally on a customer created with POST /v1/customers; customer ids are 25-character strings starting with c):
POST /v1/payment-methods
Authorization: Bearer sk_live_your_secret_key
{
"type": "card",
"payment_token": "<result.token>",
"token_reference": "<result.reference>",
"card": { "last4": "4242", "brand": "visa", "exp_month": 12, "exp_year": 2029 },
"name": "Jenny Rosen",
"customer_id": "c1m9k2x7e0001qz8f4h2b6n3p"
}
→ 201 { "id": "pcivault_9f1c…", "object": "payment_method", "type": "card", ... }
use "id" as "source" on POST /v1/charges — now or months laterAmounts are in cents (5000 = $50.00). Send an Idempotency-Key so a retried request cannot charge twice.
Step 3: Read the result
A card charge is answered synchronously:
201 Created
{
"id": "TRgk3Nq7vZ2cXb8wY1mLpR4d",
"object": "charge",
"amount": 5000,
"currency": "USD",
"status": "succeeded",
"description": "Order #1234",
"source": "<result.token>",
"created": "2026-10-27T16:00:00.000Z",
"metadata": { "order_id": "1234" }
}A decline is not a charge object but an error:
400 Bad Request
{
"error": {
"type": "card_error",
"code": "charge_failed",
"message": "Your card was declined. Please try a different card or contact your bank."
}
}| status | Meaning |
|---|---|
| succeeded | The card was charged. Store the id; it is what POST /v1/refunds takes as charge. |
| pending | An ACH debit (bank account) that has not cleared yet. Wait for charge.succeeded or poll GET /v1/charges/{id}. |
| failed | Seen on GET /v1/charges/{id} and charge.failed, with failure_code and failure_message. |
| canceled | Fully refunded or voided. |
Next Steps
- Charges reference — every request and response field
- hlhunt.js — the browser tokenization library
- ACH & bank accounts — pending debits, microdeposit verification, returns
- Webhook payloads — what charge.succeeded and charge.failed carry
- Refunds — refund a charge