cwupid

API v1

Cwupid API

Score a photograph from your own server. Access is by application and every key is issued against a Cwupid account; using it is subject to the API terms and acceptable use, which decide what you may and may not apply a score to.

Base URL and authentication

Every request goes to https://cwupid.com/v1 over HTTPS and carries your key as a bearer token: Authorization: Bearer cw_live_…. Keys are long-lived and are shown once when issued, so store yours where you keep other secrets and never ship it in a client application. Revoking a key takes effect on its next request. The browser session cookie is deliberately not accepted.

Check the API is open

GET /v1/status needs no key and answers whether the API is serving, which model is available, and the upload ceiling. It answers when the API is closed too, so a client can tell "not yet" from "broken".

Score a photograph

POST /v1/scores takes multipart/form-data with an image field — JPEG, PNG or WebP — and requires an Idempotency-Key header of 16 to 128 characters. It costs one credit, reserved before the photograph reaches the scorer and returned automatically if the action fails. A refused submission is never charged.

The response is always 202. When the score lands inside the submit window the result is attached to that same response; otherwise you get a queue position and poll. One shape, either way:

curl https://cwupid.com/v1/scores \
  -H "Authorization: Bearer $CWUPID_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -F [email protected]

Retrying with the same Idempotency-Key returns the original submission rather than charging twice. The same key presented with a different photograph is refused with 409 rather than answered — it is safer to refuse than to answer about a request you did not send.

Read a score

GET /v1/scores/{score_id} returns the current state, and the result once it is terminal. A score belonging to another account answers 404, not 403: the two are indistinguishable on purpose.

Be told instead of asking

Polling is a poor fit for a server, so register an endpoint with PUT /v1/webhook and Cwupid will POST each finished score to it. The URL must be https on a public host. The response returns a signing secret, once per registration; changing the URL rotates it, because a secret that survives a change of destination is one the old destination could still forge with.

Each delivery carries cwupid-event (score.succeeded or score.failed), cwupid-delivery, and cwupid-signature in the form t=1757372400,v1=<hmac>. Verify it before trusting the body. Compute an HMAC-SHA256 of the string <t>.<raw request body> using your signing secret, base64url-encoded, and compare it to the v1 value in constant time. Reject a timestamp that is not recent — that is what stops a captured delivery being replayed at you forever.

Answer 2xx to accept a delivery. Anything else is retried with backoff — 30 seconds, then two minutes, eight, thirty-two, and just over two hours — and after six failures the endpoint is disabled and the reason is readable from GET /v1/webhook. A disabled webhook costs you nothing but the notifications: every score is still there to read with GET /v1/scores/{score_id}. DELETE /v1/webhook removes the endpoint.

What a score says, and what it does not

The result carries a standardized score and a percentile, and it carries the qualifications that make those numbers mean something. reference_population names the population the percentile is read against. percentile_valid says whether it holds for this particular answer — where it is false, the value must not be shown to a reader as a percentile. support says where the upload sits relative to the population the grid was built on, and carries a note when it sits outside it. n_persons_detected reports how many people were found; the score is about one of them.

When the gender classifier sits near its boundary the scorer declines to pick one, and you get results_by_gender with both readings instead of a single result. Handle both shapes.

A score is an experimental prediction about a photograph. It is not a measurement of a person's attractiveness, identity, character, or worth, and the terms require that it is never presented as one.

The model

One model is sold: cwupid-1.1, a distilled single head, which is also what cwupid.com scores with. It is experimental — it did not meet the acceptance threshold its own research programme set, and it is served under a recorded disposition that says so.

Its percentile is the percentile of the ensemble score it predicts, read against that ensemble's reference population, rather than a percentile of a population this model was itself calibrated on. An estimate is less dispersed than the thing it estimates, so its percentiles sit closer to the middle: measured on held-out data, a spread ratio of 0.95 for female and 0.88 for male subjects. Read model_version back with every score rather than assuming values compare across versions.

Credits and limits

GET /v1/credits returns your balance, the account's daily and monthly ceilings, and this key's own daily ceiling if it has one. The lower of the account's and the key's applies. Read it before a batch.

API work runs in its own queue lane with a bounded share of scoring capacity. That bound is deliberate: it is what stops a batch here from making cwupid.com refuse a customer. It also means sustained throughput is that lane's share rather than the whole pool's. A 503 with code queue_full means the lane is at its ceiling, nothing was charged, and retrying shortly is the right response — not an error to alert on.

Errors

Failures are application/problem+json with a stable code, a human-readable title and detail, and a request_id worth quoting to support. Branch on code — the wording may be improved without a version change. insufficient_credits and key_limit_reached mean top up or wait; queue_full and scoring_paused mean retry; invalid_model, payload_too_large and unsupported_media_type mean fix the request. None of them charge you.

The full contract

The machine-readable specification is an OpenAPI 3.1 document kept in the repository at docs/api/cwupid-api-v1.yaml, and it is the authority where this page and it disagree. Questions, and anything that looks wrong, go to [email protected].