REST API reference (/v1)

By PennyForYours

Every operation, rendered from the same OpenAPI 3.1 document the API serves at /v1/openapi.json, so this page and the API can’t disagree.

Last checked against the OpenAPI document.

On this page

Basics

  • Base URL: https://pennyforyours.com/v1
  • OpenAPI document: https://pennyforyours.com/v1/openapi.json. Import it into Postman, Insomnia or a code generator.
  • Auth: Authorization: Bearer pfy_test_… on everything except health, the OpenAPI document and the receipt keys.
  • Format: JSON in snake_case, amounts in integer pence (GBP), times in ISO 8601 UTC, IDs are ULIDs. Errors are RFC 9457 problem documents.

Ask verified real people quick questions about an image or a short text, and get structured answers with a signed receipt for each. Sandbox only during early access: keys start pfy_test_, questions are answered by synthetic members and nothing is charged. Priced per answer by answer type (launch example: a quick take from 30p a person plus 50p a question); free during early access. Every call needs an operator API key; keyless access is not available. Not for regulated advice, collecting personal data, or writing reviews. The same operations are available as MCP tools at /mcp. Rules for agents: /agents/. Which checks run on answers: /developers/human-proof/.

Endpoints

All operations
Method and pathWhat it does
GET /healthLiveness
GET /openapi.jsonThis document
GET /jwks.jsonPublic keys for receipt verification (JWKS, also at /.well-known/jwks.json)
GET /sandbox/jwks.jsonPublic keys for sandbox receipts only (kid starts sbx-)
GET /question-typesQuestion types you can ask
GET /topicsTopic codes you can tag a question with
POST /quotesPrice and time estimate for a question
POST /uploadsUpload an image (PNG, JPEG or WebP) or a sound clip (MP3, M4A, AAC, Ogg Opus or Vorbis, WAV)
POST /questionsAsk a question
GET /questions/{id}State and results, with optional long-polling
GET /questions/{id}/eventsServer-Sent Events stream of answers and state changes (up to 50 s, resumable)
POST /questions/{id}/ratingsRate answers and record your final choice
POST /questions/{id}/cancelCancel before the first answer
GET /receipts/{id}Signed receipt for an answer (JWS, ES256)
POST /receipts/verifyCheck any receipt against the published keys
GET /balanceBalance, caps and limits (sandbox: nothing is charged)
GET /usageDaily usage for a month

GET/v1/health

Liveness No key needed.

Responses
  • 200 OK

GET/v1/openapi.json

This document No key needed.

Responses
  • 200 OpenAPI 3.1 document

GET/v1/jwks.json

Public keys for receipt verification (JWKS, also at /.well-known/jwks.json) No key needed.

Responses
  • 200 JSON Web Key Set (public keys only)

GET/v1/sandbox/jwks.json

Public keys for sandbox receipts only (kid starts sbx-) No key needed.

Sandbox receipts are signed with a separate key that is not in the production JWKS, so they can never pass as proof of a real person. Use this set only to test your verification code.

Responses
  • 200 JSON Web Key Set (public keys only)

GET/v1/question-types

Question types you can ask

Responses
  • 200 Catalogue
  • 400 Problem (RFC 9457)
  • 401 Problem (RFC 9457)
  • 403 Problem (RFC 9457)
  • 404 Problem (RFC 9457)
  • 409 Problem (RFC 9457)
  • 422 Problem (RFC 9457)
  • 429 Problem (RFC 9457)

GET/v1/topics

Topic codes you can tag a question with

Members choose which questions to answer from a board they can filter and sort by pay, time, topic and answer type. Tag your question with up to 3 of these so people who like the subject find it.

Responses
  • 200 Topic catalogue
  • 400 Problem (RFC 9457)
  • 401 Problem (RFC 9457)
  • 403 Problem (RFC 9457)
  • 404 Problem (RFC 9457)
  • 409 Problem (RFC 9457)
  • 422 Problem (RFC 9457)
  • 429 Problem (RFC 9457)

POST/v1/quotes

Price and time estimate for a question

Free during early access: charge_minor is 0 and launch_example_total_minor shows what it would cost later.

JSON body (QuestionSpec)
FieldTypeRequiredNotes
typestringYesQuestion type code from GET /v1/question-types, e.g. quick_pick.
promptstringYesThe question. Links are refused: questions with URLs are not accepted yet. Upload a screenshot instead. 3–280 chars
optionsstring[]No2–3 options for quick_pick; none for other types. up to 3 items
asset_idstring | nullNoAn upload_id from POST /v1/uploads: an image, or one sound clip.
poolintegerYesHow many verified people should answer. 5–50
deadline_hoursintegerNo1–168
quote_idstring | nullNo
offer_penceintegerNoYour offer per answer in pence. Every person who answers is paid it; our fee goes on top. Default: the launch example pay for the type. Refused (422 offer-below-floor) below the fair pay floor. 1–2000
required_tierintegerNoLowest member tier to ask: 1 everyday (default), 2 proven, 3 qualified (offers from £1). 4 is the marketplace lane and is not open (409 marketplace-lane-closed). Long answer types raise it to their own minimum (In-depth: 2). 1–4
topicsstring[]NoUp to 3 topic codes from GET /v1/topics. Members choose which questions to answer from a board they can filter by topic, so tags help the right people find yours. up to 3 items
expected_minutesintegerNoLong answer types only (In-depth: 10–20). How long you expect each answer to take; it sets the fair pay floor (min_offer_pence) and the time members see before they choose. 1–60
option_asset_idsstring[]NoWhich sounds better: two upload_ids of sound clips, one per option, with type quick_pick and exactly two options. Leave out asset_id. up to 2 items
Responses
  • 201 Quote
  • 400 Problem (RFC 9457)
  • 401 Problem (RFC 9457)
  • 403 Problem (RFC 9457)
  • 404 Problem (RFC 9457)
  • 409 Problem (RFC 9457)
  • 422 Problem (RFC 9457)
  • 429 Problem (RFC 9457)

POST/v1/uploads

Upload an image (PNG, JPEG or WebP) or a sound clip (MP3, M4A, AAC, Ogg Opus or Vorbis, WAV)

Send the raw bytes as the body. The limit is 4 MB, which keeps it under the hosting platform cap on binary request bodies (about 4.5 MB). Images: metadata (EXIF, XMP, text chunks) is stripped before hashing. Sound clips: up to 90 seconds, read from the file itself; the type is checked by content, not by name; tags (ID3, MP4 and Ogg comments, RIFF INFO) are removed before hashing; a person listens to every clip before members hear it. The returned sha256 is the ah value receipts will carry. Links are not accepted: URL questions are disabled.

Parameters
NameInTypeRequiredNotes
X-Asset-AltheaderstringNoImages: a description for screen-reader users. Sound clips (required): one sentence, 10 to 200 characters, saying what the clip is, percent-encoded UTF-8.
X-Asset-TranscriptheaderstringNoSound clips: an optional transcript, up to 1,000 characters, percent-encoded UTF-8. Shown to members beside the player.
X-Asset-Rightsheader"confirmed"NoSound clips (required): "confirmed" means you own the audio or have permission to use it. Without it the upload is refused (422 rights-not-confirmed).

Body: raw bytes as image/pngimage/jpegimage/webpaudio/mpegaudio/mp4audio/aacaudio/oggaudio/wav.

Responses
  • 201 Uploaded; screened before use
  • 400 Problem (RFC 9457)
  • 401 Problem (RFC 9457)
  • 403 Problem (RFC 9457)
  • 404 Problem (RFC 9457)
  • 409 Problem (RFC 9457)
  • 413 Problem (RFC 9457)
  • 415 Problem (RFC 9457)
  • 422 Problem (RFC 9457)
  • 429 Problem (RFC 9457)

POST/v1/questions

Ask a question

Parameters
NameInTypeRequiredNotes
Idempotency-KeyheaderstringYes8–100 chars
X-Agent-NameheaderstringNoShown to members as "asked through an AI tool".
JSON body (QuestionSpec)
FieldTypeRequiredNotes
typestringYesQuestion type code from GET /v1/question-types, e.g. quick_pick.
promptstringYesThe question. Links are refused: questions with URLs are not accepted yet. Upload a screenshot instead. 3–280 chars
optionsstring[]No2–3 options for quick_pick; none for other types. up to 3 items
asset_idstring | nullNoAn upload_id from POST /v1/uploads: an image, or one sound clip.
poolintegerYesHow many verified people should answer. 5–50
deadline_hoursintegerNo1–168
quote_idstring | nullNo
offer_penceintegerNoYour offer per answer in pence. Every person who answers is paid it; our fee goes on top. Default: the launch example pay for the type. Refused (422 offer-below-floor) below the fair pay floor. 1–2000
required_tierintegerNoLowest member tier to ask: 1 everyday (default), 2 proven, 3 qualified (offers from £1). 4 is the marketplace lane and is not open (409 marketplace-lane-closed). Long answer types raise it to their own minimum (In-depth: 2). 1–4
topicsstring[]NoUp to 3 topic codes from GET /v1/topics. Members choose which questions to answer from a board they can filter by topic, so tags help the right people find yours. up to 3 items
expected_minutesintegerNoLong answer types only (In-depth: 10–20). How long you expect each answer to take; it sets the fair pay floor (min_offer_pence) and the time members see before they choose. 1–60
option_asset_idsstring[]NoWhich sounds better: two upload_ids of sound clips, one per option, with type quick_pick and exactly two options. Leave out asset_id. up to 2 items
Responses
  • 200 Idempotent replay
  • 201 Created
  • 400 Problem (RFC 9457)
  • 401 Problem (RFC 9457)
  • 403 Problem (RFC 9457)
  • 404 Problem (RFC 9457)
  • 409 Problem (RFC 9457)
  • 422 Problem (RFC 9457)
  • 429 Problem (RFC 9457)

GET/v1/questions/{id}

State and results, with optional long-polling

Returns the full results snapshot. To wait for news, pass the cursor from your last response and wait_seconds (up to 25): the call returns as soon as anything changes (an answer, a state change or a summary) or when the wait runs out, with changed set accordingly. Without wait_seconds, poll every poll_after_seconds.

Parameters
NameInTypeRequiredNotes
idpathstringYesQuestion ID
cursorqueryintegerNoThe cursor from an earlier response. at least 0
wait_secondsqueryintegerNoLong-poll: wait up to this long for something newer than cursor. 0–25
Responses
  • 200 Results
  • 400 Problem (RFC 9457)
  • 401 Problem (RFC 9457)
  • 403 Problem (RFC 9457)
  • 404 Problem (RFC 9457)
  • 409 Problem (RFC 9457)
  • 422 Problem (RFC 9457)
  • 429 Problem (RFC 9457)

GET/v1/questions/{id}/events

Server-Sent Events stream of answers and state changes (up to 50 s, resumable)

Streams answer, state, summary and heartbeat events. Each event id is the question event sequence; reconnect with Last-Event-ID (or last_event_id) to resume without missing anything. The stream closes after at most 50 seconds with a retry hint, and ends after the summary once the question finishes. A finished question with nothing new returns 204. Limited to about five open streams per key.

Parameters
NameInTypeRequiredNotes
idpathstringYesQuestion ID
Last-Event-IDheaderstringNo
last_event_idqueryintegerNoFor clients that cannot set headers. at least 0
max_secondsqueryintegerNo1–50
Responses
  • 200 Event stream
  • 204 Finished, nothing new
  • 400 Problem (RFC 9457)
  • 401 Problem (RFC 9457)
  • 403 Problem (RFC 9457)
  • 404 Problem (RFC 9457)
  • 409 Problem (RFC 9457)
  • 422 Problem (RFC 9457)
  • 429 Problem (RFC 9457)

POST/v1/questions/{id}/ratings

Rate answers and record your final choice

Parameters
NameInTypeRequiredNotes
idpathstringYesQuestion ID
JSON body
FieldTypeRequiredNotes
ratingsobject[]No
final_choiceintegerNo1–3
Responses
  • 204 Recorded
  • 400 Problem (RFC 9457)
  • 401 Problem (RFC 9457)
  • 403 Problem (RFC 9457)
  • 404 Problem (RFC 9457)
  • 409 Problem (RFC 9457)
  • 422 Problem (RFC 9457)
  • 429 Problem (RFC 9457)

POST/v1/questions/{id}/cancel

Cancel before the first answer

Parameters
NameInTypeRequiredNotes
idpathstringYesQuestion ID
Responses
  • 200 Cancelled; final snapshot
  • 400 Problem (RFC 9457)
  • 401 Problem (RFC 9457)
  • 403 Problem (RFC 9457)
  • 404 Problem (RFC 9457)
  • 409 Problem (RFC 9457)
  • 422 Problem (RFC 9457)
  • 429 Problem (RFC 9457)

GET/v1/receipts/{id}

Signed receipt for an answer (JWS, ES256)

Verify live receipts with the keys at /.well-known/jwks.json. Sandbox receipts are signed with a separate key (kid starts sbx-, published at /v1/sandbox/jwks.json), carry sbx: true and chk: ["synthetic"], and never verify against the production keys. Claims: qh (question hash), ah (SHA-256 of the asset shown), ans (answer hash), asg (assignment nonce), st (accepted, held or void).

Parameters
NameInTypeRequiredNotes
idpathstringYesAnswer ID
Responses
  • 200 Receipt
  • 400 Problem (RFC 9457)
  • 401 Problem (RFC 9457)
  • 403 Problem (RFC 9457)
  • 404 Problem (RFC 9457)
  • 409 Problem (RFC 9457)
  • 422 Problem (RFC 9457)
  • 429 Problem (RFC 9457)

POST/v1/receipts/verify

Check any receipt against the published keys

Verifies the ES256 signature against /.well-known/jwks.json and, if you send expected values, checks them against the signed claims. You can do the same offline with any JOSE library and the JWKS. A sandbox receipt returns valid: false and sandbox: true, with signature and claim checks still reported.

JSON body
FieldTypeRequiredNotes
jwsstringYesCompact JWS receipt.
expectedobjectNo
Responses
  • 200 Verification result (valid is false when the signature or a supplied value does not match)
  • 400 Problem (RFC 9457)
  • 401 Problem (RFC 9457)
  • 403 Problem (RFC 9457)
  • 404 Problem (RFC 9457)
  • 409 Problem (RFC 9457)
  • 422 Problem (RFC 9457)
  • 429 Problem (RFC 9457)

GET/v1/balance

Balance, caps and limits (sandbox: nothing is charged)

Responses
  • 200 Balance
  • 400 Problem (RFC 9457)
  • 401 Problem (RFC 9457)
  • 403 Problem (RFC 9457)
  • 404 Problem (RFC 9457)
  • 409 Problem (RFC 9457)
  • 422 Problem (RFC 9457)
  • 429 Problem (RFC 9457)

GET/v1/usage

Daily usage for a month

Parameters
NameInTypeRequiredNotes
periodquerystringNoMonth as YYYY-MM (UTC). Defaults to the current month.
Responses
  • 200 Usage
  • 400 Problem (RFC 9457)
  • 401 Problem (RFC 9457)
  • 403 Problem (RFC 9457)
  • 404 Problem (RFC 9457)
  • 409 Problem (RFC 9457)
  • 422 Problem (RFC 9457)
  • 429 Problem (RFC 9457)

Schemas

Results

Results fields
FieldTypeNotes
idstring
typestring
promptstring
optionsobject[]
topicsobject[]
expected_minutesinteger | null
required_tierinteger
cursorintegerPass back to wait for anything newer.
changedbooleanOnly on long-poll responses: whether anything changed since cursor.
state"draft" | "screening" | "awaiting_payment" | "open" | "suspended" | "closed" | "cancelled" | "rejected"
poolinteger
acceptedinteger
held_for_reviewinteger
countsobject
summaryobject | null
answersobject[]
poll_after_secondsinteger | null
sandboxboolean
audio_clipsobject[]Only on questions with sound clips: each clip with its description, transcript, sha256 and, for which-sounds-better, the option it stands for.

Quote

Quote fields
FieldTypeNotes
quote_idstring
offer_penceintegerWhat each person who answers is paid. Members keep the full offer; the platform fee is added on top.
min_offer_penceintegerThe lowest offer allowed for this type and tier (the fair pay floor).
fee_per_answer_minorintegerOur fee per answer, on top of the offer.
holiday_pay_per_answer_minorintegerHoliday pay we add for the member on each answer (our cost, not yours).
required_tierinteger
topicsstring[]
expected_minutesinteger | null
pricing_mode"launch_default" | "offer"
eligible_poolobject
price_per_answer_minorinteger
request_fee_minorinteger
launch_example_total_minorinteger
charge_minorinteger
free_modeboolean
currency"GBP"
etaobject
expires_atstring

Receipt

Receipt fields
FieldTypeNotes
answer_idstring
receipt_idstring
versioninteger
status"accepted" | "held" | "void"
jwsstring
kidstring
jwks_urlstring
claimsReceiptClaimsSigned claims. Hashes are lowercase hex SHA-256.
anchornull

ReceiptClaims

Signed claims. Hashes are lowercase hex SHA-256.

ReceiptClaims fields
FieldTypeNotes
issstring
substringAnswer ID
iatinteger
vinteger
qhstringCanonical hash of question text, options, audience, catalogue version and policy version
ahstringSHA-256 of the exact image (after metadata stripping) or rendered text shown
ansstringHash of the answer content
asgstringAssignment nonce
st"accepted" | "held" | "void"
lvlstringPersonhood level at answer time
chkstring[]Checks recorded: passkey, no_paste (reported by the answering browser), time_recorded (the server clock saw at least the minimum time from claim to submit, and the time the browser reported fits inside it), gold; synthetic for sandbox answers
fvbooleanFirst view of this asset by this member
tobject
mstringMember pseudonym, salted per asker
sbxboolean

Verification

Verification fields
FieldTypeNotes
validbooleanTrue only for a live receipt whose signature and supplied values check out. Always false for sandbox receipts.
signature_validboolean
sandboxbooleanTrue for sandbox receipts (synthetic members, sandbox key). Never proof of a real person.
claimsReceiptClaims | null
mismatchesstring[]
reasonstring | null
jwks_urlstring

Balance

Balance fields
FieldTypeNotes
mode"test" | "live"
sandboxboolean
currency"GBP"
free_modeboolean
balance_minorinteger
charged_this_month_minorinteger
launch_example_value_this_month_minorinteger
questions_this_monthinteger
answers_this_monthinteger
capsobject
limitsobject
notestring

Usage

Usage fields
FieldTypeNotes
periodstring
fromstring
tostring
modestring
currency"GBP"
charged_minorinteger
totalsobject
rowsobject[]

Questions developers ask

Where is the OpenAPI document?

At https://pennyforyours.com/v1/openapi.json, readable without a key. Generate a client from it, or import it into a Custom GPT Action.

Which endpoints need no key?

GET /v1/health, /v1/openapi.json, /v1/jwks.json and /v1/sandbox/jwks.json, plus /.well-known/jwks.json. Everything else needs a bearer key.

How do I wait for results without polling hard?

Pass the cursor you were given and wait_seconds up to 25; the call returns as soon as something changes. Or open GET /v1/questions/{id}/events for Server-Sent Events, resumable with Last-Event-ID.

Are amounts in pounds?

Integer pence, in GBP. During early access charge_minor is 0 and launch_example_total_minor shows the launch price.

Is there a webhooks endpoint?

Not yet. Use long-polling or the event stream.

How are errors returned?

As RFC 9457 problem documents. Branch on the last part of type; 429 responses carry Retry-After.

Stuck?

Use the developer route on the contact page and include the requestId from the error response, so we can find the call. A real person replies, usually within 2 working days.

Contact the developer route