adoptrdevelopers
Examples for
Rescue sign in
On this page
API for rescues · v1

Book seats on ADOPTR from your own app.

Read upcoming departures and your own animals, hold seats, and turn them into animals with their papers. Build on the sandbox, then go live.

Get a test key
Examples for
Base URL
https://sandbox.adoptr.uk/api/v1
Auth
Authorization: Bearer adk_test_…
Format
JSON, UTF-8, days as YYYY-MM-DD
Limits
60 reads and 20 writes a minute per key

Start in three steps

  1. 01Get a test key. Sign in as your rescue at adoptr.uk/rescue/api and make one. Not working with ADOPTR yet? Email contact@adoptr.uk.
  2. 02Make your first call. List the sandbox's departures with the request here.
  3. 03Pass the go-live checklist, then go live. Twelve calls your app makes on the sandbox. When all are ticked, ask for a live key on the same page.
curl https://sandbox.adoptr.uk/api/v1/departures \
  -H "Authorization: Bearer $ADOPTR_API_KEY"
200 OK
{
  "departures": [
    { "leaves_on": "2026-12-01", "status": "open", "seats_free": 18, "deadlines": { "closes_on": "2026-11-24" } }
  ]
}

Build with an AI agent

Using Claude, Cursor or another coding agent? Give it the instructions for agents (adoptr.uk/developers/agent.md, or "Copy instructions" on the guide). They tell it what to read, how to keep the key safe and how to pass the go-live checklist on the sandbox. It stops before going live: a person at your rescue asks for the live key.

  • https://adoptr.uk/developers.md: this guide as Markdown.
  • https://adoptr.uk/llms.txt: the index for agents.
  • https://adoptr.uk/api/v1/openapi.json: OpenAPI 3.1, for Postman or a generated client.
Instructions for your agent
You are connecting our rescue's app to the ADOPTR API for rescues.

Read these first, and trust them over anything you remember:
- Guide: https://adoptr.uk/developers.md
- OpenAPI 3.1: https://adoptr.uk/api/v1/openapi.json

Rules
- Work only on the sandbox: https://sandbox.adoptr.uk/api/v1. Never call https://adoptr.uk/api/v1.
- Read the key from the environment variable ADOPTR_API_KEY (it starts adk_test_). Never hard-code, print, log or commit it.
- Call the API from our server only. It doesn't answer browsers.
- Send a new Idempotency-Key (a UUID) with every POST and DELETE. When you retry the same action, send the same key and the same body, byte for byte.
- Branch on error.code, never on error.message. On 429 and idempotency_running, wait Retry-After seconds, then retry.
… 24 more lines. Copy them all, or open agent.md.

How a booking goes

  1. See the dates. GET /departures: deadlines and seats free.
  2. Hold seats. POST /holds, before you know the animals.
  3. Add each animal. POST /holds/{id}/animals: one seat becomes one animal.
  4. ADOPTR takes it from there. Papers, the CHED, the route and the family's tracking link.

Keys

Every call sends your key: Authorization: Bearer <key>. A key belongs to your rescue: every answer holds only your animals and your seats, never another rescue's.

  • Test keys (adk_test_…) work only on the sandbox. Make them yourself at adoptr.uk/rescue/api.
  • Live keys (adk_live_…) work only on adoptr.uk. Ask for one on the same page once your app passes the go-live checklist; ADOPTR approves it and says what it may do. It is shown once, when you open it.
  • Every key reads. A key holds and gives back seats only with hold, and adds animals only with confirm: animals go on held seats, so a key with confirm has hold too.
  • Keep the key on your server, in an environment variable such as ADOPTR_API_KEY. Anyone holding it can book seats for your rescue. The API doesn't answer browsers, so it can't live in a web page or a phone app. If a key leaks, revoke it on the same page and make another.

Sandbox

https://sandbox.adoptr.uk/api/v1, with a test key. Made-up departures, put back as new every night at 03:00 UTC. Nothing there reaches a real family, van or border.

  • Four departures: a full one (seats_free 0), one with the dogs' Brucella window open today, an empty one weeks away, and a closed one (past closes_on).
  • Three animals of your own, there as soon as you make your first test key: one ready, one without a passport, one on the waiting list. Their microchips start 999.
  • Hold seats on the empty departure: the others are full, closed, or already too late for an animal sent without its rabies vaccine.

Departures

GET /departures: the departures listed on adoptr.uk that haven't left, and any still on the road with your animals, soonest first. A departure is named by the day it leaves Romania (leaves_on).

  • status: open; closed (past closes_on, no animal can join); on_the_road; done; not_listed (not on adoptr.uk, shown because your animals are on it).
  • confirmed: enough animals booked for the van to go on this day; until then the day is an estimate.
  • deadlines.closes_on: the last day an animal can join. rabies_by: the last day a rabies vaccine counts. brucella_window_from/_to: when to take dogs' Brucella samples; last_brucella_sample: the last day a sample is accepted.
  • seats_free: seats anyone can still ask for, counted as adoptr.uk counts its places for the UK: the departure's UK places less the animals going to the UK, the seats held and the places kept for rescues; never more than the van's empty crates (animals going to the EU take crates too).
  • yours: your animals on it, your seats held (each hold with its id and expires_at), and places ADOPTR keeps for you not filled yet.
200 OK
{
  "departures": [
    {
      "leaves_on": "2026-12-01",
      "uk_entry": "2026-12-03",
      "last_drop_off": "2026-12-04",
      "status": "open",
      "confirmed": false,
      "deadlines": {
        "closes_on": "2026-11-24",
        "rabies_by": "2026-11-10",
        "brucella_window_from": "2026-11-09",
        "brucella_window_to": "2026-11-13",
        "last_brucella_sample": "2026-11-13"
      },
      "zones": [],
      "seats_free": 18,
      "yours": { "animals": 2, "seats_held": 0, "holds": [], "places_kept": 0 }
    }
  ]
}

One departure

GET /departures/{leaves_on}: the same fields in departure, and animals: yours on it.

curl
curl https://sandbox.adoptr.uk/api/v1/departures/2026-12-01 \
  -H "Authorization: Bearer $ADOPTR_API_KEY"

Your animals

GET /animals: your animals on departures still ahead or on the road, and on the waiting list (leaves_on null), by id. Filters: leaves_on=YYYY-MM-DD, papers=ready|missing|blocked. Pages: limit (1 to 200, 100 by default), then after=<next_after> while next_after isn't null.

GET /animals/{id}: one of yours, in animal.

  • papers: ready, missing (still to come, to_do says what) or blocked (can't make this departure); null, with to_do empty, for an animal on the waiting list.
  • The adopter's address, phone and email are never sent back, only their name and postcode.
curl
curl "https://sandbox.adoptr.uk/api/v1/animals?papers=missing&limit=50" \
  -H "Authorization: Bearer $ADOPTR_API_KEY"
200 OK
{
  "animals": [
    {
      "id": 2,
      "name": "Max",
      "species": "dog",
      "sex": "male",
      "chip": "999000000100001",
      "born_on": "2025-07-01",
      "leaves_on": "2026-12-01",
      "destination": "GB",
      "papers": "missing",
      "to_do": [{ "item": "passport", "state": "missing", "note": "needed" }],
      "passport": null,
      "rabies": { "on": "2026-07-16", "valid_until": "2027-07-16" },
      "brucella": { "sampled_on": "2026-11-10", "result": "negative" },
      "adopter": { "name": "Priya Desai", "postcode": "BS8 4PP", "address_confirmed": true },
      "ched_ref": null,
      "apha": null
    }
  ],
  "next_after": null
}

Seats

Your key does these only when ADOPTR allowed them: hold for holding and giving back seats, confirm for adding animals. Send an Idempotency-Key with each (see Retries).

Hold seats

POST /holds: hold seats on a departure that is open, before its animals are known.

  • leaves_on (required): the day the van leaves Romania.
  • seats (required): 1 to 99, within seats_free and your rescue's seat limit: the seats your app holds at once on one departure (5 unless ADOPTR set another). A seat that becomes an animal no longer counts, so you can hold again.
  • expires_at: when the seats go back, an ISO time with its offset. 7 days if left out; at most 14 days, never after the end of the departure's closes_on (midnight, London time), at least an hour ahead. In the last hour of closes_on the departure takes no new hold (departure_closed).
  • note: for you and the ADOPTR team, up to 500 characters.
curl -X POST https://sandbox.adoptr.uk/api/v1/holds \
  -H "Authorization: Bearer $ADOPTR_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"leaves_on":"2026-12-28","seats":3,"note":"for the litter"}'
201 Created
{
  "hold": { "id": 41, "leaves_on": "2026-12-28", "seats": 3, "expires_at": "2026-12-04T18:00:00.000Z" },
  "seats_free": 15
}

Your holds

GET /holds: your holds still counting, as { "holds": [ { "id", "leaves_on", "seats", "expires_at" } ] }.

Give seats back

DELETE /holds/{id}: gives back every seat of a hold. Animals already added stay.

200 OK
{ "released": { "id": 41, "seats": 2 } }

Add an animal

POST /holds/{id}/animals: one seat of the hold becomes this animal, going to the UK. A hold of several seats keeps the rest; the answer's hold is its id, or null once its last seat is taken.

  • Required: name, species (dog or cat), chip (15 digits; spaces are dropped). Everything else can follow later through ADOPTR; to_do in the answer says what's still missing.
  • Days are YYYY-MM-DD. born_on, microchipped_on, rabies_on and brucella_sampled_on can't be in the future. A field the API doesn't know is refused.
  • Brucella (brucella_sampled_on, brucella_tube, brucella_result): once the sample is taken, in the departure's Brucella window. Leave them out until then.
  • Refused with papers_blocked (and to_do) when the animal already can't make this departure, for example a rabies vaccine too recent: the seat is kept.
  • ADOPTR asks the adopter to confirm their address before the CHED, as for every animal.

The animal is the whole animal, as GET /animals/{id} gives it (shortened here). Once its last seat is an animal, the hold is gone: GET /holds no longer lists it, and its id answers not_found.

JSON
{
  "name": "Pufi",
  "species": "dog",
  "sex": "male",
  "chip": "999000000777001",
  "born_on": "2025-03-12",
  "microchipped_on": "2025-05-01",
  "passport": "RO-9777001",
  "rabies_on": "2026-07-01",
  "rabies_vaccine": "Rabisin",
  "rabies_batch": "A123",
  "rabies_valid_until": "2027-07-01",
  "adopter": "Charlotte Evans",
  "adopter_address": "4 Bramble Way, Taunton",
  "adopter_postcode": "TA1 1AA",
  "adopter_phone": "+44 7700 900777",
  "adopter_email": "charlotte@example.com"
}
201 Created
{
  "animal": {
    "id": 57,
    "name": "Pufi",
    "papers": "missing",
    "to_do": [{ "item": "brucella", "state": "missing", "note": "sample by 11 Dec" }]
  },
  "hold": 41
}

Retries

Send an Idempotency-Key header with every POST and DELETE: 1 to 100 printable characters, no spaces, yours to choose (a UUID, for example). Sent again with the same key and the same body, byte for byte, within a day, the API answers as the first time and does nothing twice, a refusal such as not_enough_seats included. A call refused before it starts (a body that isn't valid, a bad key, too large) isn't kept: put it right and send it with the same key. Use a new key for a new action, and the same key when you retry one.

  • The same key with another body: idempotency_conflict.
  • While the first call is still running: idempotency_running. Send it again after Retry-After seconds.

Errors and limits

Errors are { "error": { "code", "message" } }. Branch on code; the message is for people and can change. Some errors say more: seats_free (with not_enough_seats), limit and held (with over_seat_limit), to_do (with papers_blocked).

StatuscodeWhen
400bad_requestA day, a filter or a field isn't valid
401unauthorizedNo key, or a key that isn't valid or was revoked
401wrong_environmentA test key on adoptr.uk, or a live key on the sandbox
403forbiddenThe key can't do this
404not_foundNot there, or not yours
409departure_closedNot open: not listed, left, or past closes_on
409not_enough_seatsFewer seats free than asked (seats_free says how many)
409over_seat_limitOver your seats held at once on one departure (limit, held)
409hold_expiredThe hold ended (expires_at passed), or its last seat was taken meanwhile
409chip_takenThe microchip is already registered with ADOPTR
409idempotency_runningThe first call with this Idempotency-Key is still running: wait Retry-After seconds
413too_largeThe body is over 16 KB
422papers_blockedThe animal can't make this departure (to_do says why)
422idempotency_conflictThe key was used for another request
429rate_limitedOver the limits: wait Retry-After seconds

Per key: 60 reads and 20 writes a minute, 5,000 calls a day.

ADOPTR keeps your calls 30 days, to help when something goes wrong: what was asked and any error, without the adopter's address, phone and email.

Go-live checklist

Your app makes these twelve calls on the sandbox, with your test keys. Each ticks itself at adoptr.uk/rescue/api when the sandbox sees it; the ticks stay when the sandbox is reset, and start again once a live key is approved, so each app passes with its own calls. A key that only reads needs the first four; one that holds seats, the first nine; one that adds animals, all twelve. Hold on the empty departure. Your app should also show your people what each answer means, not only make the call.

  1. GET /departures answers 200.
  2. GET /departures/{leaves_on} answers 200.
  3. GET /animals?limit=1, then again with after set to next_after.
  4. GET /animals/{id} answers 200.
  5. POST /holds on the empty departure answers 201, sent with an Idempotency-Key.
  6. The same POST /holds, sent again with the same key and body, gets the same answer.
  7. POST /holds on the full departure (seats_free 0) answers 409 not_enough_seats.
  8. POST /holds on the closed departure answers 409 departure_closed.
  9. DELETE /holds/{id} answers 200. Then hold 3 seats again, for the next three.
  10. POST /holds/{id}/animals with only name, species and a new chip answers 201.
  11. POST /holds/{id}/animals with rabies_on before microchipped_on answers 422 papers_blocked.
  12. POST /holds/{id}/animals with the chip of one of your sandbox animals answers 409 chip_taken.

Versions

New fields can appear in v1 at any time: ignore those you don't know. A change that would break your app comes as /api/v2, and v1 keeps working at least 6 months after it.