Troubleshooting
Status decoder, webhook debugging, and the integration failures that actually happen.
HTTP status decoder
| Code | What it means | Most common cause |
|---|---|---|
| 200 | Success, nothing new created | Idempotent replay, or a successful no-op DELETE |
| 201 | Success, record created | First-time request |
| 400 | Bad request | Missing required field, malformed JSON, invalid URL or hash |
| 401 | Authentication failed | No Authorization header, or a key that is unknown, revoked, or expired |
| 403 | Refused | Test-mode key (TEST_MODE_UNAVAILABLE), partner account not active, or extension_required. Never cross-partner access |
| 404 | Not found | Wrong id, or the resource belongs to another partner. Identical responses on purpose |
| 409 | State conflict | CONSENT_PENDING, CONSENT_DECLINED, duplicate_publication, and uploader_not_solo_active on the content and upload-session routes. POST /v1/collab-scenes returns that last code as a 400, so match on code together with the route you called |
| 422 | Unprocessable | Right shape, wrong state: not_greenlit, already_owner, invalid_status, or an account with no linked Easy2257 user |
| 429 | Rate limited | 300 requests per 60 seconds per API key, shared across all of /api/v1 |
| 500 | Server error | Retry with exponential backoff |
Switch on the status and on code, never on message text. See Errors for the response shape.
Everything returns 403
You are calling with a test-mode key. Check the response body for "code": "TEST_MODE_UNAVAILABLE".
There is no sandbox, so ez_test_ keys are rejected before any handler runs. New partner accounts are created with a test key, which is why this is the most common first-integration failure. Create a live key at partner.easy2257.com/api-keys and deploy that.
If the key starts with ez_live_ and you still get 403, your partner account is not active. Contact api@easy2257.com.
404 on an id you know exists
The resource belongs to a different partner account, or the id is wrong. Easy2257 answers both cases identically so the API cannot be walked to discover which creators are on which platform.
Check that the key you are calling with belongs to the same partner account that created the resource. Two environments pointed at two different partner accounts is the usual cause: the account was provisioned with one key and read with another.
Webhook not arriving
Work through in order:
- Have you registered an endpoint? Deliveries go only to endpoints created at
partner.easy2257.com/webhooks. ThecallbackUrlyou passed toPOST /v1/solo-accountsorPOST /v1/collab-scenesis stored for your reference and subscribes you to nothing. A handler written without this step never fires. - Does the endpoint subscribe to that event type? An endpoint carries an event list, or
*for everything. An event outside the list is not delivered and no delivery row is created. - Is the URL reachable and public? Easy2257 posts from AWS with no IP allowlist to configure, but a URL that resolves to a private or internal address is rejected at delivery time as an SSRF guard, and a VPN-only or strict WAF setup will block it.
- Did your endpoint return
2xxwithin 30 seconds? The delivery times out at 30 seconds and is treated as a failure even if your handler eventually finished. Return2xximmediately and process asynchronously. - Check the delivery log. Every attempt is recorded in the portal with HTTP status, attempt count, and final state (
DELIVERED,FAILED,EXHAUSTED). Delivery is attempted immediately, then retried at 1 minute, 5 minutes, 30 minutes, and 2 hours. After 5 attempts it is abandoned asEXHAUSTED. - Send a test ping. The portal's test button fires a real signed
test.pingat the endpoint, which proves the whole path end to end.
Webhook signature verification keeps failing
This is the most common integration bug. Five things to check:
- Raw body, not parsed JSON. HMAC the exact bytes received. Do not
JSON.parse()and re-serialize. Per framework:- Next.js App Router:
await req.text(), notawait req.json() - Express:
express.raw({ type: "application/json" })on this route only, notexpress.json() - Flask:
request.get_data(), notrequest.get_json() - FastAPI:
await request.body(), notawait request.json() - Laravel:
$request->getContent(), not$request->all() - ASP.NET Core:
Request.EnableBuffering(), then read the stream
- Next.js App Router:
- The signed payload is
{t}.{rawBody}, not the body alone. Taketfrom the signature header and concatenate timestamp, a literal dot, then the body. - The digest is lowercase hex, not base64.
- Compare in constant time.
timingSafeEqual,hmac.compare_digest,hash_equals, orCryptographicOperations.FixedTimeEquals. A plain==is a timing-attack risk. - The secret is the per-endpoint
whsec_webhook secret, not your API key. Reveal it under the endpoint in the portal.
Retries are re-signed with a fresh timestamp, so a stored signature never matches a later delivery. Deduplicate on event.id, which is stable across attempts.
Working code for Node, Express, Flask, FastAPI, Laravel, plain PHP, and C# is in Webhook Signatures.
409 CONSENT_PENDING or CONSENT_DECLINED on content
The address you provisioned already belonged to an Easy2257 user, so Easy2257 emailed that person to ask whether they approve the connection. Until they approve, their identity record is not linked to your platform and nothing can be logged against it.
Fix: hold the content and wait for solo_account.verified, which fires only after approval. CONSENT_DECLINED is terminal for that address: the creator refused, and they are never emailed about your platform again. Do not loop on re-provisioning, which is throttled to one approval email per creator per day and will not change the answer.
409 uploader_not_solo_active on content
A different failure wearing the same status. This is not about consent: the creator approved the link, or never needed to. Their own Easy2257 account has stopped being active, most often a lapsed subscription, so there is no verified identity record to attach the depiction to.
Fix: send the creator back through onboarding. This is the one 409 on this route that waiting does not clear, so a queue that holds and retries every 409 will hold this one forever. Switch on code, not on the status. Takedowns are exempt by design: DELETE keeps working on an inactive account, because a lapsed subscription is not a reason to refuse a removal request.
422 "Account has no linked EZ2257 user"
You called POST /api/v1/solo-accounts/{id}/content for an account that has no Easy2257 user behind it yet.
Fix: wait for the solo_account.verified webhook before any /content call. Note that subscribed_pending_id means "paid, ID not yet verified": it is not a green light, and unlocking there is the classic version of this bug.
DELETE returns { "deleted": false }
The depiction is addressed by externalContentId. If the original POST /content did not include one, there is no key to remove it by, and DELETE reports an honest no-op rather than removing the wrong row.
Fix going forward: always send externalContentId when logging. For rows already logged without one, email api@easy2257.com for a one-off backfill.
A 404 on the same call means something different: the accountId is not one of yours.
403 extension_required when registering a publication
You are not the partner that created the scene, so you must call POST /api/v1/collab-scenes/{sceneId}/extend first. It takes no request body: the sceneId itself is the authorization, and it is the id the scene's creating partner shares with you when the syndication is arranged. The extension records your platform against the scene and charges the $2 extension fee; after that, publications register normally.
Check fee.charged in the /extend response. A failed charge does not revoke the extension, and a re-POST retries the unpaid fee without charging twice. A 422 with already_owner means the scene is already yours and needs no extension.
Certificate verification says the certificate is fine, but the content is gone
Offline JWKS verification proves signature, issuer, and expiry. It cannot see a revocation, and a revoked certificate still verifies cryptographically. It cannot see a supersession either: a certificate replaced at annual renewal, or when a producer reissued the sealed package after a correction, still passes an offline check and never appears on the revocation list.
Fix: sync GET /api/v1/productions/revoked-certificates daily and treat a listed productionId as authoritative over your stored certificate. To tell whether a certificate that verifies offline is still the one that stands, read GET /api/v1/productions/{productionId}/certificates and take the one marked CURRENT.
What to include in a support ticket
- Your partner id, from the portal
- The approximate UTC timestamp of the request
- The request body with PII redacted, plus the response status and body
- For webhook issues: the event id (
evt_...) and your endpoint URL
Email api@easy2257.com.