Skip to main content
Both hosts serve the same contract. The environment column is the name your key is bound to, which is what GET /v1/health reports back. Integrate against the sandbox, then change one config value to go live: read the whole base URL from config, version prefix included, rather than compiling it in. One caveat: the sandbox runs against the production dataset. Users created with a sandbox key are real profiles in the same database, and the isolation is the key’s environment rather than a separate copy of the data. Use throwaway addresses on a domain you own for sandbox users, and ask us when you want them cleaned up.

Headers

Errors

error.code is stable and machine-readable, so branch on it rather than on message. message is for your logs and should not be shown to end users. details is present only where it adds something, and its shape varies by code. Those nine are the published application errors. Four more are framework-level codes that a malformed or unsupported request can trigger, and a strict switch on error.code will hit them during integration: Per-item rejections are not errors. A batch containing unusable items still returns 200, and the items come back in rejected[] with a reason; see The response. error.code means the whole request failed.

Retries and idempotency

Every endpoint is safe to retry, so a timeout never needs special handling:
  • POST /users is idempotent on email. A retry returns the same user_id rather than creating a second profile.
  • POST /users/{user_id}/media is safe to re-send. Photos that matched upsert on your asset id; photos that matched nothing are re-evaluated rather than duplicated. See Incremental syncs.
  • POST /users/{user_id}/sync-complete is idempotent; see Telling FanFeed a sync finished.
  • The GET endpoints are reads.
Retry 429 after Retry-After and 5xx with exponential backoff. Do not retry a 4xx other than 429; the request will fail the same way again.

Timestamps

ISO 8601, UTC, on everything you send. One exception on the way back: an event’s starts_at_local is the local date and time at the venue with no offset. Do not convert it. It is already the time the user experienced, and shifting it will show someone a 7:30 p.m. concert at 2:30 a.m.

Pagination

Cursor-based. Pass next_cursor from the previous response as cursor. has_more tells you when to stop. Cursors are opaque; do not parse them.

Operational endpoints

Two more endpoints sit outside the five that carry the integration. Neither is part of the API reference, and neither touches user data. GET /v1/ping needs no key and is never rate-limited. Returns:
Use it to prove the base URL and TLS before a key has been issued. GET /v1/health requires a valid key, but is deliberately not rate-limited, so it still answers while you are being throttled. It echoes back the key’s partner_id and environment along with the rate-limit policy in force for that key:
This is how you confirm which environment your key is bound to, and it is the endpoint to call after a 429 to see what the limit actually is; see Rate limiting.