Last updated

Tracking & webhooks

Webhooks tell you when something changed; GET /v1/payments/{id} tells you what is true now.

The lifecycle

accepted ──► in-review ──► in-progress ──► completed ──► refunded
    │            │              │
    ▼            ▼              ▼
 rejected     canceled        failed

scheduled ──► in-progress ──► …
    │
    ▼
 canceled
StatusMeaning
acceptedPassed all validations and accepted for processing.
in-reviewHeld for compliance or manual review before release.
scheduledAccepted and queued for a future execution date.
in-progressReleased for processing; funds are moving.
completedFunds delivered to the beneficiary.
rejectedTerminal. Declined before processing; no funds moved.
failedTerminal. Processing started but could not complete; funds returned.
canceledTerminal. Stopped while held; no funds moved. You can cancel a payment yourself while it is in-review or scheduled (see Cancelling a payment); the canceled webhook is sent when you do.
refundedTerminal. A completed payment was returned in full.

A payment never re-enters an earlier state. failureReason is populated for rejected, failed, and canceled. Treat the enum as open — tolerate statuses you don't recognize.

Reading a payment

GET /v1/payments/{paymentId} — scope payments:read. This is the authoritative view: reconcile against it whenever you miss a delivery or receive events out of order.

curl 'https://public-sandbox.tryjeeves.com/v1/payments/7a3e5f21-9d64-4c18-b0a7-1f52e8c3d9b6' \
  -H "Authorization: Bearer $JEEVES_ACCESS_TOKEN"

A payment belonging to another customer returns 404 payment_not_found, not 403 — the API never confirms the existence of a resource you can't access. originalQuoteId is non-null only when the payment was requoted; in that case quoteId names the quote that actually priced it.

The webhook

Jeeves sends payment.status.updated to your registered callback URL on every status change. The callback URL, signing secret, optional mTLS, and IP allowlist are configured in the Jeeves web app — there is no registration API.

{
  "eventId": "evt_c91d20",
  "eventType": "payment.status.updated",
  "occurredAt": "2026-09-10T15:08:30Z",
  "data": {
    "paymentId": "7a3e5f21-9d64-4c18-b0a7-1f52e8c3d9b6",
    "status": "in-progress",
    "failureReason": null
  }
}

Payloads carry the payment id, status, and failure reason only — never beneficiary or banking details.

Verify before you trust

  1. Read X-Jeeves-Timestamp and reject the delivery if the skew exceeds 5 minutes.
  2. Compute HMAC-SHA256(secret, timestamp + "." + rawBody), hex-encoded, and compare to X-Jeeves-Signature with a constant-time comparison.
  3. Compute the HMAC over the raw request body, before any JSON parsing or re-serialization — re-encoding changes the bytes and breaks the signature.

Delivery semantics

PropertyWhat it means for you
At-least-onceThe same event may arrive more than once — deduplicate on eventId (also the X-Jeeves-Event-Id header).
UnorderedReconcile on occurredAt and treat GET /v1/payments/{id} as the source of truth. Never assume the latest delivery reflects the latest state.
10-second windowRespond 2xx within 10 seconds, then process asynchronously. Anything else (or a timeout) is retried with exponential backoff for about 26 hours.
Additive changesTolerate unrecognized status values and unrecognized fields.
Not rate-limitedWebhook deliveries don't count against your API rate limits.

Attaching a receipt

Once a payment exists you can attach a receipt to it for reconciliation, audit and dispute. It is stored against the payment and appears alongside it in the Jeeves app, exactly as a receipt uploaded from the app does.

POST /v1/payments/{paymentId}/receipts — scope payments:write. The only multipart/form-data endpoint in this API: send a single file part.

curl -X POST 'https://public-sandbox.tryjeeves.com/v1/payments/7a3e5f21-9d64-4c18-b0a7-1f52e8c3d9b6/receipts' \
  -H "Authorization: Bearer $JEEVES_ACCESS_TOKEN" \
  -F 'file=@payment-receipt-8821.pdf'
{
  "receiptId": "0f9e8a05-12b1-438e-a95f-edf39969e884",
  "paymentId": "7a3e5f21-9d64-4c18-b0a7-1f52e8c3d9b6",
  "fileName": "payment-receipt-8821.pdf",
  "contentType": "application/pdf",
  "sizeBytes": 184320,
  "createdAt": "2026-10-07T09:14:33Z"
}
  • One receipt per request, and it appends. Send a single file part; posting again adds another receipt rather than replacing the first.
  • The file is checked by its content, not its name. Renaming a .exe to .pdf does not get it through. Supported: pdf, jpeg, jpg, png, tiff, up to 5 MB.
  • Receipts are stored as documents only — they are never read, parsed or put through OCR.
  • No Idempotency-Key, and one that is sent is ignored. Nothing moves money here, so a duplicate upload costs you a duplicate receipt, not a duplicate payment.

Errors follow the usual envelope: 404 payment_not_found for an unknown payment or one belonging to another customer, 415 unsupported_media_type for an unsupported file type, 413 payload_too_large above 5 MB, 422 validation_failed when the payment's status does not accept receipts, and 400 validation_failed when no file part is sent.

Statements & exports

For statement-style exports of completed payment activity, the Transactions API's GET /v1/payments/transactions lists completed payment transactions by date range with cursor pagination.