Troubleshooting
Symptom first, then the cause we see most, then the fix. Every error body the
API returns carries a request_id — include it when you write to us. Errors
from the gateway in front of the API carry none; see API errors.
| Symptom | Most likely cause | Fix |
|---|---|---|
401 on every call |
Key from the other environment, revoked, or malformed header | Check the prefix (sk_test_ vs sk_live_) against the base URL; send Authorization: Bearer sk_…. unauthorized |
400 with Idempotency-Key header is required on POST |
POST without the header | Send a stable key from your system on every POST. bad-request |
409 idempotency-key-reused |
The key belongs to another body on this operation: a request that succeeded, is still running, or ended in a 5xx or a timeout |
Repeat the original body to get the original result, or use a new key for a new request, including a corrected body after a 5xx or a timeout. idempotency-key-reused |
| Same POST created two resources | Different Idempotency-Key on the retry |
Reuse the key on retries; for churches, send your reference so identity does not depend on the key |
404 for a resource you just created |
The key belongs to another partner, or the id was typed | 404 is "not found or not yours". Check the key's partner. not-found |
400 church_id is not registered for this partner |
Your platform never registered the church, or it is in the other environment | Register or import the church first and use the id from the response. bad-request |
This link can't be opened, or a blocked frame with a frame-ancestors console error, when your page opens the flow |
The page's site is not in your allowed origins. Scheme, host and port must match exactly: http://localhost:3000 is not http://localhost:5173 |
Add the site with PUT /v1/allowed_origins; it reaches the flow within a few minutes. The link is not spent: if GET /v1/donor_sessions/{id} still says created, open the same url again. Open the flow on your page |
410 opening the hosted flow |
The session expired (30 minutes) or was already used, or the page's site is not in your allowed origins | Check GET /v1/allowed_origins first; otherwise create a new session; never reuse a URL. gone |
429 with a problem body |
More than 600 requests in a minute | Wait Retry-After seconds, then retry. rate-limited |
429, 500 or 503 whose body is only a message |
The gateway in front of the API: its throttle, shared by every partner, or a timeout | Retry with exponential backoff and the same Idempotency-Key; log the UTC time and the operation. API errors |
400 with an HTML body, or 403 with an empty body to a browser preflight |
The gateway refused the request before the API saw it: a path it cannot parse (such as %zz), or a call made from a web page |
Percent-encode ids when you build the path; call the API from your backend. Neither response carries X-Request-Id. API errors |
503 with a problem body (unavailable) |
The operation is not enabled in this environment | Do not retry in a loop; check the environment. unavailable |
Other 5xx with a problem body |
Our fault | Retry with the same Idempotency-Key. internal |
| The feed keeps returning the same events | The cursor from the last page is not being sent back, or a null replaced it |
Send next_cursor as since_cursor on the next poll and store it after each page that has events. An empty page can return next_cursor: null: keep the cursor you already stored. |
| No webhook arrived | Endpoint unreachable, or signature rejected on your side and the retry budget ran out | Run POST /v1/webhook_endpoints/{id}/test — it reports the status your endpoint returned. Reconcile activity from GET /v1/events and donation.updated / donor.updated from GET /v1/financial_events, each with its own cursor |
| Donor sees "Paycheck giving isn't enabled for this church yet" | Roster hasn't verified the church's legal setup and receiving account | Complete both; on a new church, payout_ready=true on GET /v1/churches/{id} confirms it. legal setup, status after verification, when a church is ready |
| Signature never verifies | Verifying the parsed JSON instead of the raw bytes, a secret with its whsec_ prefix removed or decoded, the header read with the wrong case (Node lowercases header names: use req.headers["roster-signature"]), or clock skew over five minutes |
HMAC the exact bytes received as {t}.{raw body}, keyed with the whole whsec_ string; fix the clock. Copy the tested verifier and check it against the test vector. webhooks |
The flow finished on your page but no enrollment.activated |
Payroll not connected yet, or its confirmation has not reached Roster | Trust the feed, not the end of the flow. On sandbox, connect the payroll in the test window inside the flow, or call POST /sandbox/simulate/payroll-connected |
| Certification item stays off | The scenario did not reach the feed | The screen reads the feed, not a checkbox — run the scenario end to end. certification |
Still stuck? Write to us with the call's X-Request-Id (req_…; on an error
it is also request_id in the body) and the Idempotency-Key you sent — those
two identify the exact call on our side.