Skip to main content
POST
/
bookings
Create a booking
curl --request POST \
  --url https://api.safarapi.com/api/partner/v1/bookings \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: <idempotency-key>' \
  --header 'X-Signature: <x-signature>' \
  --header 'X-Timestamp: <x-timestamp>' \
  --data '
{
  "quote_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
  "partner_reference": "<string>",
  "traveler": {
    "first_name": "<string>",
    "last_name": "<string>",
    "email": "jsmith@example.com",
    "phone": "<string>",
    "customer_reference": "<string>"
  },
  "rooms_allocation": [
    {
      "room_index": 1,
      "occupants": [
        {
          "age": 8
        }
      ]
    }
  ],
  "payment_confirmation": {
    "bank_payment_ref": "<string>",
    "paid_amount_total": 1116,
    "paid_amount_net": 949.9,
    "paid_at": "2023-11-07T05:31:56Z"
  },
  "special_requests": "<string>"
}
'
import requests

url = "https://api.safarapi.com/api/partner/v1/bookings"

payload = {
    "quote_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
    "partner_reference": "<string>",
    "traveler": {
        "first_name": "<string>",
        "last_name": "<string>",
        "email": "jsmith@example.com",
        "phone": "<string>",
        "customer_reference": "<string>"
    },
    "rooms_allocation": [
        {
            "room_index": 1,
            "occupants": [{ "age": 8 }]
        }
    ],
    "payment_confirmation": {
        "bank_payment_ref": "<string>",
        "paid_amount_total": 1116,
        "paid_amount_net": 949.9,
        "paid_at": "2023-11-07T05:31:56Z"
    },
    "special_requests": "<string>"
}
headers = {
    "Idempotency-Key": "<idempotency-key>",
    "X-Timestamp": "<x-timestamp>",
    "X-Signature": "<x-signature>",
    "Authorization": "Bearer <token>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.text)
const options = {
  method: 'POST',
  headers: {
    'Idempotency-Key': '<idempotency-key>',
    'X-Timestamp': '<x-timestamp>',
    'X-Signature': '<x-signature>',
    Authorization: 'Bearer <token>',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    quote_id: '3c90c3cc-0d44-4b50-8888-8dd25736052a',
    partner_reference: '<string>',
    traveler: {
      first_name: '<string>',
      last_name: '<string>',
      email: 'jsmith@example.com',
      phone: '<string>',
      customer_reference: '<string>'
    },
    rooms_allocation: [{room_index: 1, occupants: [{age: 8}]}],
    payment_confirmation: {
      bank_payment_ref: '<string>',
      paid_amount_total: 1116,
      paid_amount_net: 949.9,
      paid_at: '2023-11-07T05:31:56Z'
    },
    special_requests: '<string>'
  })
};

fetch('https://api.safarapi.com/api/partner/v1/bookings', options)
  .then(res => res.json())
  .then(res => console.log(res))
  .catch(err => console.error(err));
<?php

$curl = curl_init();

curl_setopt_array($curl, [
  CURLOPT_URL => "https://api.safarapi.com/api/partner/v1/bookings",
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_ENCODING => "",
  CURLOPT_MAXREDIRS => 10,
  CURLOPT_TIMEOUT => 30,
  CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST => "POST",
  CURLOPT_POSTFIELDS => json_encode([
    'quote_id' => '3c90c3cc-0d44-4b50-8888-8dd25736052a',
    'partner_reference' => '<string>',
    'traveler' => [
        'first_name' => '<string>',
        'last_name' => '<string>',
        'email' => 'jsmith@example.com',
        'phone' => '<string>',
        'customer_reference' => '<string>'
    ],
    'rooms_allocation' => [
        [
                'room_index' => 1,
                'occupants' => [
                                [
                                                                'age' => 8
                                ]
                ]
        ]
    ],
    'payment_confirmation' => [
        'bank_payment_ref' => '<string>',
        'paid_amount_total' => 1116,
        'paid_amount_net' => 949.9,
        'paid_at' => '2023-11-07T05:31:56Z'
    ],
    'special_requests' => '<string>'
  ]),
  CURLOPT_HTTPHEADER => [
    "Authorization: Bearer <token>",
    "Content-Type: application/json",
    "Idempotency-Key: <idempotency-key>",
    "X-Signature: <x-signature>",
    "X-Timestamp: <x-timestamp>"
  ],
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
  echo "cURL Error #:" . $err;
} else {
  echo $response;
}
package main

import (
	"fmt"
	"strings"
	"net/http"
	"io"
)

func main() {

	url := "https://api.safarapi.com/api/partner/v1/bookings"

	payload := strings.NewReader("{\n  \"quote_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n  \"partner_reference\": \"<string>\",\n  \"traveler\": {\n    \"first_name\": \"<string>\",\n    \"last_name\": \"<string>\",\n    \"email\": \"jsmith@example.com\",\n    \"phone\": \"<string>\",\n    \"customer_reference\": \"<string>\"\n  },\n  \"rooms_allocation\": [\n    {\n      \"room_index\": 1,\n      \"occupants\": [\n        {\n          \"age\": 8\n        }\n      ]\n    }\n  ],\n  \"payment_confirmation\": {\n    \"bank_payment_ref\": \"<string>\",\n    \"paid_amount_total\": 1116,\n    \"paid_amount_net\": 949.9,\n    \"paid_at\": \"2023-11-07T05:31:56Z\"\n  },\n  \"special_requests\": \"<string>\"\n}")

	req, _ := http.NewRequest("POST", url, payload)

	req.Header.Add("Idempotency-Key", "<idempotency-key>")
	req.Header.Add("X-Timestamp", "<x-timestamp>")
	req.Header.Add("X-Signature", "<x-signature>")
	req.Header.Add("Authorization", "Bearer <token>")
	req.Header.Add("Content-Type", "application/json")

	res, _ := http.DefaultClient.Do(req)

	defer res.Body.Close()
	body, _ := io.ReadAll(res.Body)

	fmt.Println(string(body))

}
HttpResponse<String> response = Unirest.post("https://api.safarapi.com/api/partner/v1/bookings")
  .header("Idempotency-Key", "<idempotency-key>")
  .header("X-Timestamp", "<x-timestamp>")
  .header("X-Signature", "<x-signature>")
  .header("Authorization", "Bearer <token>")
  .header("Content-Type", "application/json")
  .body("{\n  \"quote_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n  \"partner_reference\": \"<string>\",\n  \"traveler\": {\n    \"first_name\": \"<string>\",\n    \"last_name\": \"<string>\",\n    \"email\": \"jsmith@example.com\",\n    \"phone\": \"<string>\",\n    \"customer_reference\": \"<string>\"\n  },\n  \"rooms_allocation\": [\n    {\n      \"room_index\": 1,\n      \"occupants\": [\n        {\n          \"age\": 8\n        }\n      ]\n    }\n  ],\n  \"payment_confirmation\": {\n    \"bank_payment_ref\": \"<string>\",\n    \"paid_amount_total\": 1116,\n    \"paid_amount_net\": 949.9,\n    \"paid_at\": \"2023-11-07T05:31:56Z\"\n  },\n  \"special_requests\": \"<string>\"\n}")
  .asString();
require 'uri'
require 'net/http'

url = URI("https://api.safarapi.com/api/partner/v1/bookings")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Idempotency-Key"] = '<idempotency-key>'
request["X-Timestamp"] = '<x-timestamp>'
request["X-Signature"] = '<x-signature>'
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"quote_id\": \"3c90c3cc-0d44-4b50-8888-8dd25736052a\",\n  \"partner_reference\": \"<string>\",\n  \"traveler\": {\n    \"first_name\": \"<string>\",\n    \"last_name\": \"<string>\",\n    \"email\": \"jsmith@example.com\",\n    \"phone\": \"<string>\",\n    \"customer_reference\": \"<string>\"\n  },\n  \"rooms_allocation\": [\n    {\n      \"room_index\": 1,\n      \"occupants\": [\n        {\n          \"age\": 8\n        }\n      ]\n    }\n  ],\n  \"payment_confirmation\": {\n    \"bank_payment_ref\": \"<string>\",\n    \"paid_amount_total\": 1116,\n    \"paid_amount_net\": 949.9,\n    \"paid_at\": \"2023-11-07T05:31:56Z\"\n  },\n  \"special_requests\": \"<string>\"\n}"

response = http.request(request)
puts response.read_body
{ "booking_number": "MV-AB7X92", "partner_reference": "YBANK-BOOK-001", "status": "CONFIRMED", "adventure_slug": "marrakech-3-jours-desert-sandbox", "adventure_title": "[DEMO] 3-day Sahara desert circuit from Marrakech", "travel_date": "2026-07-12", "traveler_name": "A. B.", "pricing": { "b2c_price": { "amount": "5250.00", "currency": "MAD" }, "customer_price": { "amount": "6210.00", "currency": "MAD" }, "amount_due_to_safariat": { "amount": "5162.50", "currency": "MAD" }, "awb_commission_share": { "amount": "87.50", "currency": "MAD" }, "sandbox_surcharge": { "amount": "960.00", "currency": "MAD" }, "indicative": false }, "settlement_id": null, "created_at": "2026-07-12T10:05:00Z", "traveler": { "first_name": "Amina", "last_name": "Benali", "email": "amina.benali@example.com", "phone": "+212600112233", "language": "fr", "customer_reference": "YBANK-CUST-77310" }, "rooms_allocation": [ { "room_index": 0, "occupants": [ { "type": "ADULT" }, { "type": "ADULT" }, { "type": "CHILD", "age": 8 }, { "type": "CHILD", "age": 12 } ] } ], "special_requests": null, "payment_confirmation": { "bank_payment_ref": "YBANK-PAY-2026-07-12-00891", "paid_at": "2026-07-12T10:04:30Z", "payment_method": "ACCOUNT_DEBIT" }, "snapshot_id": "5e6d7c8b-9a0f-4b1c-8d2e-3f4a5b6c7d8e", "cancellation_tiers": [ { "days_before": 30, "refund_percent": 100, "refundable_amount": { "amount": "5400.00", "currency": "MAD" } }, { "days_before": 14, "refund_percent": 50, "refundable_amount": { "amount": "2700.00", "currency": "MAD" } }, { "days_before": 7, "refund_percent": 0, "refundable_amount": { "amount": "0.00", "currency": "MAD" } } ], "voucher_url": "https://placehold.co/600x400?text=DEMO" }

Request Structure

{
  "data": {
    "type": "bookings",
    "attributes": {
      "account_id": 1,
      "location_id": 10,
      "consignment_reference": "CONS-2024-001",
      "direction": "inbound",
      "hazardous": false,
      "start_time": "2024-09-15T08:00:00Z",
      "end_time": "2024-09-15T09:00:00Z",
      "haulier": "DHL Express",
      "intake_type": "full_load",
      "container_number": "MSKU1234567",
      "storage_unit_quantity": 20,
      "notes": "Fragile goods - handle with care"
    }
  }
}

Required Fields

FieldTypeDescription
account_idintegerStock account ID (owner of the goods)
location_idintegerDock location ID (must have available_in_booking_diary enabled)
consignment_referencestringConsignment reference number
directionstringOne of: inbound, outbound
hazardousbooleanWhether the goods are hazardous
start_timedatetimeBooking start time (ISO 8601)
end_timedatetimeBooking end time (must be after start_time)

Optional Fields

FieldTypeDescription
account_referencestringAccount’s own reference
booking_referencestringBooking reference number
container_numberstringContainer number (for container intake)
haulierstringHaulier/carrier name
intake_typestringOne of: container, part_load, full_load
notesstringFree text notes
statusstringOne of: pending, arrived, departed, fail_to_arrive, cancelled (default: pending)
storage_unit_quantityintegerExpected number of storage units (e.g., pallets)

Slot Availability Validation

The system validates that the requested time slot is available before creating the booking.

Opening Times Compliance

The location (or its parent warehouse) may have opening times configured. Opening hours vary by day of week, so the system checks the specific day’s hours for both start and end times. Error Response (code 295):
{
  "errors": [
    {
      "code": 295,
      "symbol": "outside_of_opening_times",
      "details": "Provided start time is outside of opening times",
      "source": "start_time"
    }
  ]
}

No Overlapping Bookings

The system uses time range overlap detection to ensure no other booking exists at the same location during the requested period. Error Response (code 296):
{
  "errors": [
    {
      "code": 296,
      "symbol": "overlapping_bookings",
      "details": "There are overlapping bookings for the selected time slot",
      "source": "time_slot"
    }
  ]
}

Finding Available Slots Workflow

To find an available time slot before creating a booking:
  1. Query eligible locations - Find locations where available_in_booking_diary is true (via GraphQL)
  2. Query existing bookings - Retrieve bookings for the target date range and location (via GraphQL)
  3. Calculate gaps - Identify time gaps that fall within the location’s opening hours for the target day of week
  4. Create booking - POST /bookings with the chosen slot

Authorizations

Authorization
string
header
default:sk_test_DEMO0000_replace_with_your_sandbox_key
required

API key authentication. Issued by the Safariat admin or via the partner portal. The secret is shown only once at generation.

Production sk_live_* keys must additionally sign every write request (POST/PUT/DELETE) with the X-Timestamp and X-Signature headers. Sandbox sk_test_* keys are exempt from request signing: X-Signature and X-Timestamp are not required for writes in the sandbox (so the developer-portal "Try it" playground works end to end). The Idempotency-Key header remains required on writes in both environments.

Headers

Idempotency-Key
string
required

Opaque, client-generated string unique per logical operation (a UUID v4 is recommended but any non-blank value up to 255 chars is accepted). Same key + same body = the cached response is returned with the Idempotent-Replayed: true header. Same key + different body = 409 Conflict. Retained for 24 h.

Maximum string length: 255
X-Timestamp
integer<int64>
required

Unix timestamp in seconds at the time the request is issued. Validity window ±5 minutes — beyond that the request is rejected. Required for production sk_live_* keys only; not required for sandbox sk_test_* keys.

X-Signature
string
required

hex(HMAC_SHA256(secret, "{X-Timestamp}\n{METHOD}\n{path}\n{body}")). The path includes the query string. The body is the exact JSON representation sent — any reformatting invalidates the signature. Required for production sk_live_* keys only; not required for sandbox sk_test_* keys.

Pattern: ^[a-f0-9]{64}$

Body

application/json
quote_id
string<uuid>
required
partner_reference
string
required

The bank's internal reference (booking ID on the bank side). Unique per partner. Lets the bank locate the Safariat booking from its own primary key.

Required string length: 1 - 100
traveler
object
required
rooms_allocation
object[]
required
Minimum array length: 1
payment_confirmation
object
required

Payment confirmation supplied by the bank on booking creation (request input).

special_requests
string

Special requests (dietary requirements, accessibility, etc.).

Maximum string length: 1000

Response

Booking created and confirmed

booking_number
string
required
Example:

"MV-AB7X92"

partner_reference
string
required
status
enum<string>
required

The partner channel skips the PENDING status (no payment-wait window). Additional internal Safariat statuses (PAYMENT_PROCESSING, REFUND_PENDING) are never exposed on this API.

Available options:
CONFIRMED,
COMPLETED,
CANCELLED
adventure_slug
string
required
travel_date
string<date>
required
pricing
object
required

Revenue breakdown with the real surcharge recorded at booking (indicative = false).

created_at
string<date-time>
required
traveler
object
required
rooms_allocation
object[]
required
payment_confirmation
object
required

Payment confirmation echoed back on the booking (response projection). The monetary amounts (customer total, net due to Safariat, surcharge) are exposed via pricing.

snapshot_id
string<uuid>
required

ID of the adventure snapshot captured at creation (see ADR-013). Guarantees the traveler receives the experience as it was at the time of booking.

cancellation_tiers
object[]
required
adventure_title
string
traveler_name
string

Initialed (e.g. 'A. B.') for PII protection in lists.

settlement_id
string<uuid> | null

ID of the settlement this booking is included in (null if not yet invoiced).

special_requests
string | null
voucher_url
string<uri>

Pre-signed URL to the PDF voucher (7-day TTL).