Getting started
Three steps, in this order. The middle one involves a person, so it is not instant.
-
Claim the business
Send your proof of identity to
POST /api/v1/claim-my-business. This is the one endpoint that needs no key, because you do not have one yet. -
We check it, by hand
Someone reads every claim. If it holds up, your business is marked as claimed and we email you a key. If it does not, we email you about that instead. Nothing on your page changes in the meantime.
-
Use the key
Send it as
Authorization: Bearer <key>on every request after that. You can then correct your facts and post your next tour whenever you like.
Your key
A key looks like this — the prefix sct_ followed by 43 characters:
Authorization: Bearer sct_7hQ2xVn4Ld8sKpR1yTgW0cAeJm6BuZfHo3iN5vXqDwE
It is 32 bytes from a cryptographic random source, so it is not guessable and there is nothing in it to work out. Treat it exactly as you would a password.
We cannot tell you your key. We store only a SHA-256 hash of it, so after the email that delivers it, no copy exists on our side. If you lose it, email hello@supercars.tours and we will revoke the old one and issue a new one. Revoking is instant, and a revoked key stops working on its very next request.
Keeping it safe
- Server-side only. Never put the key in a web page, a mobile app, or anything a browser downloads. Anyone who can read it can edit your listing.
- Not in your git repository. Use an environment variable or your host's secret store.
- One key per system. If you want your website and your booking system to both post dates, ask for two keys. Then a leak in one can be revoked without breaking the other.
- Tell us if it leaks. Straight away, at hello@supercars.tours. There is no penalty, and a revoke-and-reissue takes a minute.
Every request is logged against your key, and every change you make is recorded against your page as operator-supplied, with the date. That log is how we can tell your corrections apart from our own.
Rate limits
Counted per key, per hour, per endpoint — not per address, so you are never throttled by
another operator sharing your hosting. Going over returns 429; wait and retry.
| Endpoint | Per hour | Counted by |
|---|---|---|
| POST /claim-my-business | 5 | your IP address (no key exists yet) |
| PATCH /update-my-details | 120 | your key |
| GET /my-next-tour | 600 | your key |
| PUT /my-next-tour | 60 | your key |
| POST /tour-dates | 60 | your key |
These are generous for the intended use, which is a handful of writes when something changes. If you need more, tell us what for.
Errors
Every failure has the same shape — an error string, plus an errors
array when individual fields are at fault:
{
"error": "Nothing was saved.",
"errors": [
{ "field": "price_band", "message": "One of: lt4k, 4-8k, 8-15k, 15k+. Or null to clear it." }
]
}
| Code | Meaning | What to do |
|---|---|---|
| 400 | The body is not valid JSON, or not the multipart form the claim endpoint expects. | Fix the request. Retrying will not help. |
| 401 | No key, or a key we do not recognise. A revoked key and a made-up key get the same answer. | Check the header. If it was working yesterday, ask us whether it was revoked. |
| 403 | The key is valid, but the business is not verified as claimed yet. | Wait for the claim to be approved. |
| 404 | No operator with that slug. | Check the slug in your page's URL. |
| 405 | Right URL, wrong method. | The message names the method to use. |
| 422 | We understood the request and refused it. Nothing was saved — not even the valid fields. | Read errors, fix every entry, send again. |
| 429 | Over the rate limit. | Wait, then retry. |
| 503 | Our end failed. Your request was fine. | Retry in a few minutes. Safe to repeat — see idempotency below. |
A 422 is all-or-nothing. If you send eight fields and one is wrong,
none of the eight is saved. This is deliberate: a half-applied update is harder to reason about
than a rejected one.
Claim your business
Send multipart/form-data with a document that shows you are who you say you are
— a company registration, a letterhead, a domain-ownership screenshot, anything a person
can check.
| Field | Required | Rules |
|---|---|---|
| slug | yes | The last part of your page's URL, e.g. alpine-drives. |
| name | yes | Your name. Up to 120 characters. |
| yes | Where the key goes if the claim is approved. Up to 200 characters. | |
| role | no | Your role at the company. Up to 120 characters. |
| message | no | Anything you want the reviewer to know. Up to 4000 characters. |
| proof | yes | One file, under 8 MB. PDF, or a photo as PNG, JPEG, WebP, AVIF or GIF. |
# One request. The slug is the last part of your page's URL. curl -X POST https://supercars.tours/api/v1/claim-my-business \ -F "slug=alpine-drives" \ -F "name=A. Director" \ -F "email=ops@example.com" \ -F "role=Operations" \ -F "message=Company registration attached." \ -F "proof=@registration.pdf"
202 Accepted means it is queued for a human, not that it is approved:
{
"claim": 41,
"operator": "alpine-drives",
"status": "new",
"note": "A person reviews every claim. If it checks out you will receive an API key by email; nothing on your page changes until then."
}
Notes worth reading before you send
- Your document is not public. It is stored under a private prefix that our public media route refuses to serve. It cannot be reached by guessing a URL.
- We identify the file by its contents, not its name or the type your
client declares. Renaming a
.exeto.pdfis rejected. SVG is refused outright — it can carry script. - Both halves of the validation come back together. If the form and the file
are both wrong, one
422tells you about both, so you are not fixing one problem per round trip. - Five claims an hour per address. Enough for a typo or two, not for scanning.
What you can change, and what you can't
This is the most important section on the page, so it comes before the endpoint that uses it.
You are the best possible source for the structured facts of your own tours — whose car, how long, what it costs, how many cars run together. You are the worst possible source for whether our author enjoyed driving with you. So the API accepts the first kind and refuses the second, by name, with a reason.
Yours to change
Send any of these to PATCH /update-my-details.
| Field | Type |
|---|---|
| website | http(s) URL, max 300 chars |
| car_policy | own · hire · both |
| length_min_days | whole number, 1–60 |
| length_max_days | whole number, 1–60 |
| price_band | see accepted values |
| convoy_size | text, max 120 chars |
| group_vibe | see accepted values |
| open_to | see accepted values |
| hotel_level | whole number of stars, 1–5 |
Ours to keep
Send any of these and the whole request is refused, with this reason.
| Field | Why not |
|---|---|
| is_published | Publishing is decided here, not by the API. |
| toured | Whether we have toured with you is our record, and cannot be set. |
| verified_on | That date records our own check of your website. |
| style_score | The luxury-to-driving position is our editorial judgement. |
| style | Derived from the style score. |
| teaser | Editorial text is written here. |
| website_facts | "What they say" is our dated transcription of your site. |
| who_for | Editorial text is written here. |
| who_not_for | Editorial text is written here. |
| field_notes | Field notes are the author's own and cannot be set. |
| slug | Changing it would break every link to your page. |
| name | A rename is an editorial change. Email us. |
| country | Your base country is part of how you are indexed. |
| logo_url | Send us a logo; it is not set through the API. |
| claimed_at | A claim is granted after verification, never self-set. |
This is not just a rule in our code. The database account that serves the API has permission to write those nine columns and no others. If we shipped a bug that tried to let you publish your own page, the database would still refuse it. We mention this because you are trusting us with your listing, and a promise backed by a permission is worth more than a promise.
If something in the editorial text is factually wrong, that is worth telling us about — email hello@supercars.tours and we will check it and re-date it. We correct facts. We don't take copy.
Update your details
Send JSON containing only the fields you want to change. It is a PATCH rather
than a PUT on purpose:
- Leave a field out and it is untouched.
- Send
nulland it is cleared.
Those two are different, so there is no way to wipe a value by forgetting to mention it.
curl -X PATCH https://supercars.tours/api/v1/update-my-details \ -H "Authorization: Bearer $SCT_KEY" \ -H "Content-Type: application/json" \ -d '{ "car_policy": "both", "length_min_days": 3, "length_max_days": 7, "price_band": "8-15k", "hotel_level": 5, "convoy_size": null }'
{
"operator": "alpine-drives",
"updated": ["car_policy", "length_min_days", "length_max_days", "price_band", "hotel_level", "convoy_size"],
"note": "Recorded, and logged against your page as operator-supplied. Editorial text and the toured label are unchanged."
}
The updated array is what actually changed. Compare it with what
you sent if you want to be certain.
Things that will be refused
- An empty body.
{}is a422, not a success. A client sending nothing has a bug, and "200, nothing changed" would hide it. - An unknown field. Named in the error rather than silently dropped, so a typo does not look like it worked.
- A backwards range.
length_min_daysabovelength_max_daysis two individually valid numbers that are jointly wrong. - A non-http URL.
javascript:anddata:are rejected, becausewebsiteis rendered as a link.
Check your next tour
Returns the next tour we hold for you, including one still waiting for moderation. A read that only showed published rows would answer "nothing" to an operator whose submission is sitting in the queue, which is the least useful true answer available.
curl -H "Authorization: Bearer $SCT_KEY" \
https://supercars.tours/api/v1/my-next-tour
{
"operator": "alpine-drives",
"next_tour": {
"title": "Stelvio & Gavia, four days",
"start_date": "2026-09-14",
"end_date": "2026-09-17",
"destination": "alps-dolomites",
"url": "https://example.com/tours/stelvio-2026",
"price_from": 6400,
"currency": "EUR",
"external_ref": "stelvio-2026-09",
"status": "pending"
},
"note": "Held as \"pending\" — a person reviews every date before it appears."
}
With nothing on file, next_tour is null and the note says so.
Finished and archived tours are never returned — "next" means next.
status is pending while it waits and
published once it is live on your page. Poll this if you want to know when it
went up; a few times a day is plenty.
Post your next tour
One tour per request. Send the whole thing; this replaces rather than patches.
| Field | Required | Rules |
|---|---|---|
| title | yes | Up to 160 characters. |
| start_date | yes | YYYY-MM-DD. |
| end_date | yes | YYYY-MM-DD, not before start_date. |
| destination | no | One of the nine codes. |
| url | no | Must start http:// or https://. Max 300 chars. |
| price_from | no | Positive, up to 99,999,999, two decimal places. |
| currency | with price | Three uppercase letters, e.g. EUR. Required whenever you send price_from — a number without a currency is not a price. |
| external_ref | no, but do | Your own identifier, up to 80 characters. See below. |
curl -X PUT https://supercars.tours/api/v1/my-next-tour \ -H "Authorization: Bearer $SCT_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Stelvio & Gavia, four days", "start_date": "2026-09-14", "end_date": "2026-09-17", "destination": "alps-dolomites", "url": "https://example.com/tours/stelvio-2026", "price_from": 6400, "currency": "EUR", "external_ref": "stelvio-2026-09" }'
{
"operator": "alpine-drives",
"inserted": 1,
"updated": 0,
"status": "pending",
"note": "Received. A person reviews every date before it appears on your page."
}
Always send an external_ref. It is your own identifier for
the tour, and it makes the request idempotent: send the same ref twice and the second
request updates the first row instead of creating a duplicate. That is what makes a
503 safe to retry, and what lets you re-post the same tour after a price change
without ending up with two of them. Without a ref, every request creates a new row.
Everything lands as pending. Nothing you send can publish itself.
A person reads every date before it appears, usually within a day or two. If a date is wrong
after it is published, just send it again with the same external_ref.
Sending an unrecognised destination returns 422 with the
offending value named, rather than a server error. Leave it out if none of the nine fits.
Sync a whole calendar
The same rules as PUT /my-next-tour, but up to
200 tours in one request. Use this if your season is already in a system that
can post to us; use PUT if you are updating one date by hand.
curl -X POST https://supercars.tours/api/v1/tour-dates \ -H "Authorization: Bearer $SCT_KEY" \ -H "Content-Type: application/json" \ -d '{ "tours": [ { "title": "Spring Alps", "start_date": "2026-04-10", "end_date": "2026-04-14", "destination": "alps-dolomites", "external_ref": "alps-2026-04" }, { "title": "Dolomites Summer", "start_date": "2026-07-02", "end_date": "2026-07-06", "destination": "alps-dolomites", "external_ref": "dolo-2026-07" } ] }'
It is one transaction and one validation pass: if any tour in the batch is invalid,
none of them is saved and the errors are indexed so you know which. An
external_ref must be unique within a single request as well as across requests.
{
"error": "Nothing was saved.",
"errors": [
{ "index": 1, "field": "end_date", "message": "The end date cannot be before the start date." }
]
}
Because every ref is an upsert, re-posting your full calendar is safe and is the simplest way to keep us in step. Nightly is fine; the limit is 60 an hour.
Accepted values
These are keys, not labels. We translate them into English and French ourselves, so send the key
exactly as written — never a translated string. Any of them accepts null to
clear the value.
| Field | Values | What it means |
|---|---|---|
| car_policy | ownhireboth |
Whether guests bring their own car, drive one of yours, or either. |
| price_band | lt4k4-8k8-15k15k+ |
Typical per-car cost. lt4k is "less than 4,000". |
| group_vibe | couples-friendlymixedyoung-fast |
Who the group tends to be, so readers can self-select. |
| open_to | all-qualifyingsingle-brandmembers |
Who may join: anyone with a suitable car, one marque only, or members of a club. |
| hotel_level | 1–5 |
Stars, as a whole number. |
The luxury-to-driving position on your page is derived from a score we set, not from any of these. That axis is our editorial judgement and is not settable — see what stays ours.
Destination codes
Nine codes, and only these nine. Anything else is a 422 naming the value you sent.
If none of them fits your tour, leave destination out entirely and tell us at
hello@supercars.tours — a genuine gap is worth a
new code.
| Code | Covers |
|---|---|
| alps-dolomites | The Alpine passes and the Dolomites, whichever country the road is in. |
| france | France, outside the Alps. |
| italy | Italy, outside the Dolomites. |
| uk | Great Britain and Northern Ireland. |
| spain-portugal | Iberia. |
| scandinavia-ice | Nordic tours, including ice driving. |
| other-europe | Anywhere else in Europe. |
| usa | North America. |
| event | Built around a fixture — a race weekend, a concours, a rally. |
Reading the public data
GET /api/v1/tours returns the published directory and needs no key at all. It is the
same data any reader sees, so you can check how your listing appears without authenticating.
Everything in it is public by definition; nothing private is exposed there.