Developer API v1
Twyft Developer API
Limited access is available only to active Business Accounts after separate enablement by Twyft. Contact support before building a production integration.
Manage API keysAuthentication and limits
Send Authorization: Bearer twft_…. Dashboard cookies and sessions never authenticate /api/v1. A key secret is displayed once, stored only as a SHA-256 hash, and stops working immediately when revoked. Resources are isolated to the key owner; a missing or another owner’s shipment has the same 404 response.
Each active key has its own rolling 60-second limit: 120 requests for standard keys or 600 requests for approved high-volume keys. Read X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, and on 429 Retry-After.
Published surface
The only accepted service slugs are iom-nextday and iom-uk-standard. iom-nextday is IM→IM; iom-uk-standard is IM→ordinary GB. Northern Ireland, Channel Islands, IM-as-GB and other international creation are unavailable.
POST /quotesGET /shipmentsPOST /shipmentsGET /shipments/{ref}GET /shipments/{ref}/label- Quotes return an available price in integer GBP pence or a truthful unavailable outcome, without creating effects.
- Lists use
limit1–100 (default 50) and zero-basedoffset, with optionalstatusandclientRef. - Creation requires
Idempotency-Key. Omitted/falseconfirmcreates a draft. True requests prepaid or separately approved account-credit confirmation and may return confirmed, credit hold, insufficient balance, or confirmation unavailable. - Replay the identical validated request and key to receive the same result without duplicate shipment, financial, provider, or operational effects. Different input with that key returns 409.
- Poll owned shipment detail for status. Download a PDF only when
labelAvailableis true; otherwise label retrieval returns 409label_not_ready.
Requests and constraints
POST /quotes
| Field | Presence | Constraint |
|---|---|---|
| service | Required | iom-nextday or iom-uk-standard |
| origin | Required | country must be IM; non-empty postcode |
| destination | Required | uppercase two-letter country and non-empty postcode; must match service route |
| parcel | Required | finite numbers: weightKg ≥ 0.01; dimensions ≥ 0.1 cm; contents optional |
| collectionDate | Optional | real YYYY-MM-DD calendar date |
POST /shipments
| Field | Presence | Constraint |
|---|---|---|
| Idempotency-Key header | Required | 1–255 characters; not whitespace-only |
| service | Required | iom-nextday (IM→IM) or iom-uk-standard (IM→ordinary GB) |
| sender | Required | name, valid email, addressLine1, town, postcode, country=IM; phone/addressLine2 optional |
| recipient | Required | name, addressLine1, town, postcode and route country; email/phone/addressLine2 optional |
| parcel | Required | weight 0.01–25 kg; dimensions ≥ 0.1 cm; contents optional, max 500 characters |
| collectionDate | Optional | real YYYY-MM-DD; omitted resolves to next eligible IM collection date |
| confirm | Optional | boolean; omitted is equivalent to false (draft) |
| clientRef | Optional | maximum 100 characters |
If collectionDate is omitted, Twyft resolves the next eligible Isle of Man collection date. A supplied date must be a real calendar date; an unavailable date produces the documented unavailable/validation outcome rather than an unexplained substitution.
Before using confirm: true, choose Prepay Balance on Account → API Keys → Payment Method and fund the balance, or have Twyft approve account credit and select it there. Insufficient prepay returns 402; no usable approved funding returns 409. A credit hold is not a confirmed shipment. Saved-card execution is not supported.
GET /shipments
| Field | Presence | Constraint |
|---|---|---|
| status | Optional | one published shipment status |
| clientRef | Optional | exact client reference filter |
| limit | Optional | integer 1–100; default 50 |
| offset | Optional | integer ≥ 0; default 0 |
GET detail and label
| Field | Presence | Constraint |
|---|---|---|
| ref | Required | opaque path value, 1–30 characters |
Responses and vocabulary
Shipment statuses: draft · credit_hold · pending · confirmed · collected · at_depot · in_transit · out_for_delivery · delivered · failed · cancelled · written_off
Quote responses distinguish available prices from unavailable reasons. Creation returns draft, confirmed with prepaid or account_credit funding, or credit hold. Representative contract responses:
{
"quoteAvailable": {
"outcome": "available",
"service": "iom-nextday",
"price": {
"currency": "GBP",
"amountPence": 1295
}
},
"quoteUnavailable": {
"outcome": "unavailable",
"reasonCode": "route_unavailable",
"message": "The selected route is unavailable."
},
"draft": {
"outcome": "draft",
"shipment": {
"ref": "shp_demo_0001",
"clientRef": "ORDER-DEMO-1",
"service": "iom-nextday",
"status": "draft",
"createdAt": "2026-01-15T10:00:00Z",
"sender": {
"name": "Example Sender",
"email": "sender@example.invalid",
"addressLine1": "1 Example Street",
"town": "Douglas",
"postcode": "IM1 1AA",
"country": "IM"
},
"recipient": {
"name": "Example Recipient",
"addressLine1": "2 Sample Road",
"town": "Douglas",
"postcode": "IM2 2BB",
"country": "IM"
},
"parcel": {
"weightKg": 1,
"lengthCm": 20,
"widthCm": 15,
"heightCm": 10
},
"price": null,
"tracking": [
{
"status": "pending",
"occurredAt": "2026-01-15T10:00:00Z",
"description": "Draft created"
}
],
"labelAvailable": false,
"carrierRef": null
}
},
"confirmedPrepaid": {
"outcome": "confirmed",
"funding": "prepaid",
"shipment": {
"ref": "shp_demo_0002",
"clientRef": "ORDER-DEMO-2",
"service": "iom-nextday",
"status": "confirmed",
"createdAt": "2026-01-15T10:05:00Z",
"sender": {
"name": "Example Sender",
"email": "sender@example.invalid",
"addressLine1": "1 Example Street",
"town": "Douglas",
"postcode": "IM1 1AA",
"country": "IM"
},
"recipient": {
"name": "Example Recipient",
"addressLine1": "2 Sample Road",
"town": "Douglas",
"postcode": "IM2 2BB",
"country": "IM"
},
"parcel": {
"weightKg": 1,
"lengthCm": 20,
"widthCm": 15,
"heightCm": 10
},
"price": {
"currency": "GBP",
"amountPence": 1295
},
"tracking": [
{
"status": "confirmed",
"occurredAt": "2026-01-15T10:05:00Z",
"description": "Booking confirmed"
}
],
"labelAvailable": false,
"carrierRef": null
}
},
"confirmedAccountCredit": {
"outcome": "confirmed",
"funding": "account_credit",
"shipment": {
"ref": "shp_demo_0002",
"clientRef": "ORDER-DEMO-2",
"service": "iom-nextday",
"status": "confirmed",
"createdAt": "2026-01-15T10:05:00Z",
"sender": {
"name": "Example Sender",
"email": "sender@example.invalid",
"addressLine1": "1 Example Street",
"town": "Douglas",
"postcode": "IM1 1AA",
"country": "IM"
},
"recipient": {
"name": "Example Recipient",
"addressLine1": "2 Sample Road",
"town": "Douglas",
"postcode": "IM2 2BB",
"country": "IM"
},
"parcel": {
"weightKg": 1,
"lengthCm": 20,
"widthCm": 15,
"heightCm": 10
},
"price": {
"currency": "GBP",
"amountPence": 1295
},
"tracking": [
{
"status": "confirmed",
"occurredAt": "2026-01-15T10:05:00Z",
"description": "Booking confirmed"
}
],
"labelAvailable": false,
"carrierRef": null
}
},
"creditHold": {
"outcome": "credit_hold",
"shipment": {
"ref": "shp_demo_0003",
"clientRef": "ORDER-DEMO-3",
"service": "iom-nextday",
"status": "credit_hold",
"createdAt": "2026-01-15T10:10:00Z",
"sender": {
"name": "Example Sender",
"email": "sender@example.invalid",
"addressLine1": "1 Example Street",
"town": "Douglas",
"postcode": "IM1 1AA",
"country": "IM"
},
"recipient": {
"name": "Example Recipient",
"addressLine1": "2 Sample Road",
"town": "Douglas",
"postcode": "IM2 2BB",
"country": "IM"
},
"parcel": {
"weightKg": 1,
"lengthCm": 20,
"widthCm": 15,
"heightCm": 10
},
"price": null,
"tracking": [],
"labelAvailable": false,
"carrierRef": null
}
}
}Executable end-to-end example
The creation response supplies SHIPMENT_REF for every later command. A confirmed request requires a funded prepay balance or separately approved account credit.
#!/usr/bin/env bash
set -euo pipefail
export BASE_URL="https://twyft.im/api/v1" # use https://shadow.twyft.im/api/v1 for acceptance
export API_KEY="twft_replace_with_the_once_displayed_secret"
curl --fail-with-body -sS "$BASE_URL/quotes" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"service":"iom-nextday","origin":{"country":"IM","postcode":"IM1 1AA"},"destination":{"country":"IM","postcode":"IM2 2BB"},"parcel":{"weightKg":1,"lengthCm":20,"widthCm":15,"heightCm":10}}' | jq .
curl --fail-with-body -sS "$BASE_URL/quotes" \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"service":"iom-uk-standard","origin":{"country":"IM","postcode":"IM1 1AA"},"destination":{"country":"GB","postcode":"M1 1AE"},"parcel":{"weightKg":1,"lengthCm":20,"widthCm":15,"heightCm":10}}' | jq .
export IDEMPOTENCY_KEY="order-demo-001"
export CREATE_BODY='{"service":"iom-nextday","sender":{"name":"Example Sender","email":"sender@example.com","addressLine1":"1 Example Street","town":"Douglas","postcode":"IM1 1AA","country":"IM"},"recipient":{"name":"Example Recipient","addressLine1":"2 Sample Road","town":"Douglas","postcode":"IM2 2BB","country":"IM"},"parcel":{"weightKg":1,"lengthCm":20,"widthCm":15,"heightCm":10,"contents":"Documents"},"clientRef":"ORDER-DEMO-001","confirm":true}'
CREATE_RESPONSE=$(curl --fail-with-body -sS "$BASE_URL/shipments" \
-H "Authorization: Bearer $API_KEY" -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" -d "$CREATE_BODY")
printf '%s\n' "$CREATE_RESPONSE" | jq .
SHIPMENT_REF=$(printf '%s' "$CREATE_RESPONSE" | jq -er '.shipment.ref | select(type == "string" and length > 0)')
export SHIPMENT_REF
# Identical replay preserves the exact body and key.
curl --fail-with-body -sS "$BASE_URL/shipments" \
-H "Authorization: Bearer $API_KEY" -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-H "Content-Type: application/json" -d "$CREATE_BODY" | jq .
curl --fail-with-body -sS "$BASE_URL/shipments?limit=50&offset=0" -H "Authorization: Bearer $API_KEY" | jq .
DETAIL_RESPONSE=$(curl --fail-with-body -sS "$BASE_URL/shipments/$SHIPMENT_REF" -H "Authorization: Bearer $API_KEY")
printf '%s\n' "$DETAIL_RESPONSE" | jq .
LABEL_AVAILABLE=$(printf '%s' "$DETAIL_RESPONSE" | jq -r 'if (.labelAvailable | type) == "boolean" then .labelAvailable else error("labelAvailable must be a boolean") end')
if [[ "$LABEL_AVAILABLE" == "true" ]]; then
curl --fail-with-body -sS "$BASE_URL/shipments/$SHIPMENT_REF/label" \
-H "Authorization: Bearer $API_KEY" -o "$SHIPMENT_REF.pdf"
else
printf 'PENDING: label is not yet available for %s\n' "$SHIPMENT_REF" >&2
fiErrors and availability
Errors use { "error": { "code": string, "message": string, "field"?: string, "details"?: object } }.
Exact codes: invalid_request · authentication_required · forbidden · shipment_not_found · label_not_ready · idempotency_conflict · confirmation_unavailable · insufficient_prepay_balance · validation_failed · rate_limited · service_unavailable · internal_error
400 malformed request; 401 missing, invalid or inactive credential; 402 insufficient prepay; 403 forbidden; 404 missing/non-owned shipment; 409 idempotency conflict, confirmation unavailable, or label not ready; 422 validation; 429 rate limit; 500 unexpected error; 503 required service unavailable.
Webhooks are not currently available. Poll shipment detail for status and use authenticated label retrieval only after labelAvailable becomes true.
