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| Status | Meaning |
|---|---|
accepted | Passed all validations and accepted for processing. |
in-review | Held for compliance or manual review before release. |
scheduled | Accepted and queued for a future execution date. |
in-progress | Released for processing; funds are moving. |
completed | Funds delivered to the beneficiary. |
rejected | Terminal. Declined before processing; no funds moved. |
failed | Terminal. Processing started but could not complete; funds returned. |
canceled | Terminal. 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. |
refunded | Terminal. 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
- Read
X-Jeeves-Timestampand reject the delivery if the skew exceeds 5 minutes. - Compute
HMAC-SHA256(secret, timestamp + "." + rawBody), hex-encoded, and compare toX-Jeeves-Signaturewith a constant-time comparison. - 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
| Property | What it means for you |
|---|---|
| At-least-once | The same event may arrive more than once — deduplicate on eventId (also the X-Jeeves-Event-Id header). |
| Unordered | Reconcile on occurredAt and treat GET /v1/payments/{id} as the source of truth. Never assume the latest delivery reflects the latest state. |
| 10-second window | Respond 2xx within 10 seconds, then process asynchronously. Anything else (or a timeout) is retried with exponential backoff for about 26 hours. |
| Additive changes | Tolerate unrecognized status values and unrecognized fields. |
| Not rate-limited | Webhook 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
filepart; posting again adds another receipt rather than replacing the first. - The file is checked by its content, not its name. Renaming a
.exeto.pdfdoes 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.