Orders

Authorize and Capture

Most Throttle checkouts authorize and capture in one step. Splitting them lets you hold funds at order time and take the money when you actually ship — and take less than you held if the final total came in lower.

The routes

RouteBodyConstraintScope
POST /api/v1/orders/{id}/authorize{ paymentId }Order must be pendingpayment_authorizations:write
POST /api/v1/orders/{id}/capture{ paymentId }Full amount onlypayment_captures:write
POST /api/v1/payments/{id}/authorizeNoneApplies the order guard when linkedpayment_authorizations:write
POST /api/v1/payments/{id}/capture{ amount? }Partial capture lives herepayment_captures:write
POST /api/v1/orders/{id}/closeNoneOnly from completedorders:write

The order-scoped and payment-scoped routes do the same underlying work. Use the order-scoped pair when you are holding an order id and want Throttle to keep the order status in step; use the payment-scoped pair when you are working from a payment.

Partial capture

Only POST /api/v1/payments/{id}/capture accepts an amount. Omit it and Throttle captures the full authorized amount; pass one and it captures exactly that, in minor units.

Capture $42.00 of a $60.00 authorization
curl -X POST https://api.usethrottle.dev/api/v1/payments/pay_88ce.../capture \
  -H "X-API-Key: $THROTTLE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 4200 }'
You get one capture per authorization
Capturing less than you authorized finalizes the payment at the captured amount — the uncaptured remainder is released back to the buyer by the processor, and there is no second capture against that authorization. Decide the final figure before you call this. If you need to bill more later, create a new payment on the order.

Over-capturing is rejected with 400 capture_amount_exceeds_authorized, and a zero or fractional amount with 400 invalid_capture_amount.

amount vs capturedAmount

After a partial capture the payment keeps two figures, and they mean different things:

Partially captured payment
{
  "data": {
    "id": "pay_88ce...",
    "status": "captured",
    "amount": 6000,
    "capturedAmount": 4200,
    "capturedAt": "2026-08-09T14:22:40.000Z",
    "currency": "USD"
  }
}
  • amount — what was authorized. It does not shrink to match the capture.
  • capturedAmount — what was actually taken, and what the buyer is charged.
Reconcile on capturedAmount
Anything that sums money — payouts, revenue reports, refund ceilings — must read capturedAmount. Reading amount overstates revenue on every partially captured order.

Full capture

The order-scoped route captures the whole authorization and keeps the order status aligned. The order must be in pending, processing, partially_fulfilled, or completed.

Capture an order's payment in full
curl -X POST https://api.usethrottle.dev/api/v1/orders/ord_9f2a.../capture \
  -H "X-API-Key: $THROTTLE_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "paymentId": "pay_88ce..." }'
Only the order-scoped routes are retry-safe
POST /orders/{id}/capture and /authorize check the payment first and treat an already-captured or already-authorized payment as a no-op, so a duplicated webhook or a retried job is harmless.

The payment-scoped routes do not. Calling POST /payments/{id}/capture on a payment that is already captured, or /authorize on one that is no longer pending, fails. If you are driving these from a queue or a webhook handler, prefer the order-scoped pair or send an Idempotency-Key.

Authorizing

POST /api/v1/orders/{id}/authorize requires the order to be pending — authorization is the first money step, so anything further along is a 400 invalid_order_state. On this route, re-authorizing an already-authorized payment is a no-op; on the payment-scoped route it is not (see above).

Cancelling an order voids any open authorization automatically, so you do not need to release a hold by hand. See Order states.

Closing an order

POST /api/v1/orders/{id}/close moves a completed order to closed, the terminal archive state. It is the only transition out of completed, and it moves no money.

Webhooks

A successful capture emits payment.captured carrying both figures, so a consumer can tell a partial capture from a full one without re-fetching:

payment.captured
{
  "type": "payment.captured",
  "data": {
    "paymentId": "pay_88ce...",
    "orderId": "ord_9f2a...",
    "amount": 4200,
    "capturedAmount": 4200,
    "authorizedAmount": 6000,
    "currency": "USD",
    "processor": "embedded",
    "processorTransactionId": "txn_..."
  }
}

A processor decline emits both payment.capture_failed and payment.failed. Handle whichever suits you, but expect both to arrive for a single failure.

Error codes

CodeHTTPMeaning
invalid_capture_amount400amount is not a positive integer.
capture_amount_exceeds_authorized400Asked for more than the authorization holds.
invalid_order_state400The order status does not permit this transition.
invalid_payment_state400The payment is past the point where this action applies.
payment_order_mismatch400That payment does not belong to the order in the path.

Full list and envelope shape on Error codes, and payment status meanings on Payment states.