Log in

API reference

Your own registration system can register exhibits in your expo and follow their status. The API belongs to one expo: it is the expo of the key you use. It only registers: once an exhibit is accepted, it is evaluated together with the rest of the expo's exhibits when you run the expo's evaluation, never at the moment it arrives.

Registering an exhibit takes two calls, like the expo's own registration: first you apply with one application per exhibit, and once that application is accepted (at once if the expo accepts registrations automatically, otherwise when the organizer accepts it) you send the exhibit. Every application is decided on its own; an exhibitor counts as accepted as soon as one of theirs is.

Before you start

  1. In the expo's Setup, add API to the registration methods.
  2. In the expo's API keys tab, create a key. You see it, and the secret that signs webhooks, once; the key starts with jpk_.

Every request carries Authorization: Bearer <key>. The limit is 60 requests per minute per key. Errors are JSON, {"detail": "..."}, with the usual status: 401 (bad or revoked key), 403 (the expo does not accept the API method, or the exhibitor withdrew), 404, 409 (the expo has no room for more exhibits, or the exhibit can no longer be changed), 422 (the request is not acceptable, the detail says why), 429.

1. Apply

POST https://api.stamp.show/api/v1/applications

{
  "exhibitor": { "email": "ana@example.com", "name": "Ana Pérez", "country": "PE", "phone": "+51 1 555 0100" },
  "external_reference_id": "exh-001",
  "title": "The 1856 issue",
  "exhibit_class": "traditional",
  "frame_count": 3,
  "default_sheet_size": "a4",
  "webhook_url": "https://your-system.example.org/stampshow-hook"
}
Field
exhibitor.emailRequired. The exhibitor is created in the expo the first time, and reused afterwards. No invitation e-mail is sent.
exhibitor.name / country / phoneOptional. country is ISO 3166-1 alpha-2. They only fill what is still empty.
external_reference_idOptional. Your own id for this application. A retried request with the same value answers the first application again (200) instead of creating another.
title, exhibit_classRequired. The class must be one the expo accepts. Both can still change when you send the exhibit.
youth_age_classRequired for the youth classes (a, b or c), not allowed for the rest.
frame_countRequired, 1 to 16. The frames are taken from the expo's package when the application is accepted, not before. If the expo accepts registrations automatically and has no room left, the application is refused (409).
default_sheet_sizeOptional: a4 (default), a3, a2 or square.
webhook_urlOptional. A public http(s) URL that hears of this application, see Webhooks.

The answer is 201:

{
  "id": "p8d2w5nq1c",
  "external_reference_id": "exh-001",
  "status": "pending",
  "exhibit_id": null,
  "title": "The 1856 issue",
  "exhibit_class": "traditional",
  "frame_count": 3,
  "default_sheet_size": "a4",
  "exhibitor_email": "ana@example.com",
  "exhibit": null,
  "created_at": "2027-03-01T10:15:00Z"
}

status is the application's: pending, accepted or rejected. If the expo accepts registrations automatically it is already accepted; otherwise it stays pending until the organizer decides on it. exhibit_id appears only once this application is accepted. Follow it with GET /api/v1/applications/{id} and GET /api/v1/applications (newest first), or with the webhook.

2. Send the exhibit

POST https://api.stamp.show/api/v1/exhibits/{exhibit_id}

You only have an exhibit_id once the application is accepted. If the expo reviews applications by hand, the organizer decides on each one in the expo's Journey, Registration phase; a rejected application never gets one. An exhibit_id that does not exist, or belongs to another expo, answers 404.

{ "exhibit": { ...an Open Exhibit document... } }

The exhibit is an Open Exhibit document (JSON Schema, examples; validate it against the schema before sending): title, class, frames and sheets, and for every sheet (and synopsis page) an image.uri we download. Its rulesets must include the expo's (FIP or APS) and it cannot have more frames than the application's frame_count. The title and class in the document replace the ones from the application, so they can change after registering. The layout of the frames is the document's, not default_sheet_size.

Images must be JPEG or PNG, at most 15 MB each, on a public host that answers without redirects. If you give a checksum, it is verified. The answer is 202 with the submission (sending the same exhibit again while it is processing or done answers it again with 200):

{
  "id": "k3m9x2qa7p",
  "exhibit_id": "w4t7c1mz8h",
  "source_id": "exh-001",
  "status": "processing",
  "error_message": null,
  "created_at": "2027-03-01T10:20:00Z",
  "exhibit": { "title": "The 1856 issue", "class": "traditional", "exhibitor_email": "ana@example.com", "application_status": "draft" },
  "evaluation": null
}

Decide an application

When the expo does not accept registrations automatically, the organizer decides in Stamp.Show. The key can decide too, for a site that manages applications itself (the WordPress plugin does): POST /api/v1/applications/{id}/accept or /reject, with no body. It answers the application as GET does. Only a pending application can be decided (409 otherwise, or when the expo's package has no room for its frames), and the usual application.accepted / application.rejected webhook follows.

Follow the exhibit

GET /api/v1/exhibits/{exhibit_id} and GET /api/v1/exhibits (the expo's submissions, newest first).

status
processingThe images are being downloaded (a failed download is retried three times).
readyThe exhibit is registered: exhibit.application_status is accepted. The organizer was told by e-mail.
failederror_message says why. See below.

evaluation stays null until the organizer evaluates the expo's exhibits. Then it shows status (pending, running, completed, failed) and, when completed, total_score, medal, criteria_breakdown, global_justification and considerations. Poll the exhibit to read it: the evaluation result is not pushed.

Read the expo's public content

Read-only endpoints with the same data, and the same visibility rules, as the expo's public site on Stamp.Show, so you can show it on a website of your own (the WordPress plugin uses them). All answer JSON, and a 404 once the expo's public window has closed. Images come back as public URLs.

Endpoint
GET /api/v1/palmares{"visible": true, "awards": [...], "special_prizes": [...]}. Each award has catalogue_number, title, exhibit_class, exhibitor, country, frame_count, medal and score. Until the organizer publishes (or the scheduled date arrives) it answers {"visible": false, "reason": "not_published" | "scheduled", "available_on": "2027-05-01" | null}.
GET /api/v1/sponsors{"items": [{"name", "category", "logo_url"}]}, in the organizer's order.
GET /api/v1/special-prizes{"items": [{"name", "donor", "conditions", "logo_url"}]}
GET /api/v1/judges{"items": [{"name", "country", "photo_url"}]}. Only judges who confirmed.
GET /api/v1/commissioners{"items": [{"name", "countries": ["PE", ...], "languages": [...]}]}. Only active commissioners; no e-mail.
GET /api/v1/catalog{"items": [{"catalogue_number", "title", "exhibit_class", "exhibitor", "country", "frame_count", "medal", "score"}]}. Accepted exhibits that are ready; medals and scores appear once the palmares is visible.

Cache what you read and refetch on the content.changed and results.published webhooks, below, instead of polling.

Webhooks

Webhooks go to a URL in two places: the webhook_url of an application (only for that application's events), and the webhook URL of the API key, set in the expo's API keys tab together with the events it should hear. One endpoint on the key is enough to hear everything. We POST it a JSON event {"event": "...", "data": {...}}. Events:

Event
application.acceptedThe application was accepted: data.exhibit_id is there, send the exhibit.
application.rejectedThe organizer rejected the application: no exhibit will be created.
exhibit.readyThe exhibit you sent is registered.
exhibit.failedIt could not be registered: data.exhibit.error_message says why.
results.published(Key URL only.) The palmares was published: data is {"resource": "palmares"}.
content.changed(Key URL only.) Sponsors, special prizes, judges, commissioners or the catalog changed: data.resource names the endpoint to read again. It carries no content, and bursts of edits arrive as one event per resource per minute.

For the application events, data is the same object GET /api/v1/applications/{id} answers.

Each request carries X-StampShow-Event, X-StampShow-Delivery (a unique id, to ignore repeats) and X-StampShow-Signature: the hex HMAC-SHA256 of the raw body keyed with your key's webhook secret (shown once, when you create the key). Answer with any 2xx; otherwise we retry up to 6 times over about nine hours. Webhooks are a convenience: GET always has the truth.

When something goes wrong

There is nothing for your system to retry by hand. Whoever can fix the problem is told by e-mail:

Example

curl -X POST https://api.stamp.show/api/v1/applications \
  -H "Authorization: Bearer jpk_..." -H "Content-Type: application/json" \
  -d @application.json

curl -X POST https://api.stamp.show/api/v1/exhibits/w4t7c1mz8h \
  -H "Authorization: Bearer jpk_..." -H "Content-Type: application/json" \
  -d @exhibit.json

Rotating a key

Rotating generates a new secret; the previous one keeps working for 48 hours so you can switch without downtime. The webhook secret does not change. Revoking disables a key at once.