Safeonward Public B2B API v1
The public B2B API lets an approved agency create onward ticket reservation PDFs from its prepaid wallet.
How the reservation works
Each order produces a real airline PNR created in the GDS, delivered as a ready-to-render reservation PDF. It is a genuine reservation, not a mock-up.
- Temporary hold. The reservation is a held (un-ticketed) booking. Typical validity is ~72 hours, and the minimum window we target before automated carrier sweeps is 48 hours. Download and use the PDF while the hold is valid.
- Automatic release. When the hold reaches its due date, Safeonward
releases it automatically — you do not need to do anything. You can also
release it early with
POST /orders/{order_id}/cancel(see below), which frees the seat and returns the credit. - Early carrier drop. If a carrier drops an un-ticketed hold earlier than expected, contact us — we handle these case by case and will either place a free replacement hold on the same route or return the credit to your wallet at no cost.
- The PDF. The reservation document is branded with the operating carrier's logo and name — there is no Safeonward branding on it — so it can be handed to your end-users as-is (see Reservation document & privacy).
Base URL
https://safeonward.com/api/v1
Authentication
All endpoints require an API key linked to an approved B2B agency wallet. Safeonward provides the key after agency access is approved.
Send the key as a Bearer token:
Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Endpoints
Wallet Balance
GET /api/v1/balance
Response:
{
"total_credits": 20,
"used_credits": 3,
"remaining_credits": 17
}
Create Order
POST /api/v1/orders
This endpoint consumes 1 credit, starts fulfillment, and waits up to PUBLIC_API_FULFILLMENT_WAIT_SECONDS seconds. The default is 120 seconds.
For integration testing, send "test_mode": true. Test mode returns a ready demo reservation with a signed PDF URL, does not contact the provider, and does not consume wallet credits.
Request:
curl -X POST https://safeonward.com/api/v1/orders \
-H "Authorization: Bearer $SAFEONWARD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from_iata": "CDG",
"to_iata": "BKK",
"departure_date": "2026-07-15",
"email": "[email protected]",
"billing_country": "FR",
"passengers": [
{
"gender": "male",
"first_name": "Jean",
"last_name": "Dupont"
}
]
}'
Test mode request:
curl -X POST https://safeonward.com/api/v1/orders \
-H "Authorization: Bearer $SAFEONWARD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"test_mode": true,
"from_iata": "CDG",
"to_iata": "BKK",
"departure_date": "2026-07-15",
"email": "[email protected]",
"passengers": [
{
"gender": "male",
"first_name": "Jean",
"last_name": "Dupont"
}
]
}'
Required fields:
| Field | Type | Notes |
|---|---|---|
from_iata |
string | 3-letter IATA code |
to_iata |
string | 3-letter IATA code, different from from_iata |
departure_date |
date | At least 10 days from today |
email |
string | Customer email |
passengers |
array | 1 to 11 passengers |
passengers.*.gender |
string | male or female |
passengers.*.first_name |
string | Minimum 2 characters |
passengers.*.last_name |
string | Minimum 2 characters |
Optional fields:
| Field | Type | Notes |
|---|---|---|
test_mode |
boolean | If true, returns a demo reservation and does not consume credits |
return_date |
date | Must be after departure_date |
phone_number |
string | E.164 format, for example +33612345678 |
billing_country |
string | 2-letter country code |
currency |
string | EUR, USD, GBP, INR, or PHP |
passengers.*.born_on |
date | Must be before today |
Ready response:
{
"order_id": "pao_abc123",
"status": "ready",
"service": "onward_ticket",
"test_mode": false,
"booking_reference": "PNR123",
"airline_reference": "DJNRBI",
"pdf_url": "https://safeonward.com/order/reservation/1/download?expires=...",
"status_url": "https://safeonward.com/api/v1/orders/pao_abc123",
"credits_remaining": 19,
"credit_refunded": false
}
Which reference do I show my customer? Use
airline_reference— it is the airline's own record locator, the code your customer and immigration officers verify on the carrier's public "Manage My Booking" / CheckMyTrip. On some content the airline exposes a locator that differs frombooking_reference(our internal PNR);airline_referencealways resolves to the verifiable code (falling back to the PNR when the carrier exposes no separate locator). Do not surfacebooking_referenceto end-users.
Pending response:
{
"order_id": "pao_abc123",
"status": "pending",
"service": "onward_ticket",
"test_mode": false,
"status_url": "https://safeonward.com/api/v1/orders/pao_abc123",
"retry_after": 5,
"credits_remaining": 19,
"credit_refunded": false
}
If the response is pending, poll status_url.
Failed response:
{
"order_id": "pao_abc123",
"status": "failed",
"service": "onward_ticket",
"test_mode": false,
"status_url": "https://safeonward.com/api/v1/orders/pao_abc123",
"credits_remaining": 20,
"credit_refunded": true,
"error": {
"code": "provider_draft_failed",
"message": "Provider refused the reservation."
}
}
If fulfillment fails before a valid reservation PDF is produced, the credit is automatically refunded.
Get Order Status
GET /api/v1/orders/{order_id}
Example:
curl -H "Authorization: Bearer $SAFEONWARD_API_KEY" \
https://safeonward.com/api/v1/orders/pao_abc123
The response uses the same shape as POST /orders.
Cancel Order
POST /api/v1/orders/{order_id}/cancel
Use this when your customer no longer needs the reservation. Safeonward releases the provider hold when one is still open and returns the credit to your wallet (as long as it has not already been refunded). The credit is returned even if the hold was already released or expired.
Example:
curl -X POST https://safeonward.com/api/v1/orders/pao_abc123/cancel \
-H "Authorization: Bearer $SAFEONWARD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"reason": "Customer no longer needs the reservation."}'
Response:
{
"order_id": "pao_abc123",
"status": "canceled",
"service": "onward_ticket",
"test_mode": false,
"status_url": "https://safeonward.com/api/v1/orders/pao_abc123",
"credits_remaining": 20,
"credit_refunded": true,
"canceled_at": "2026-06-26T12:00:00+00:00",
"cancellation": {
"code": "partner_requested",
"message": "Customer no longer needs the reservation."
}
}
The endpoint is idempotent. Calling it again for the same order returns the canceled order and does not refund another credit.
Download PDF
GET /api/v1/orders/{order_id}/pdf
If the PDF is ready, this endpoint redirects to a temporary signed PDF download URL.
If the PDF is not ready:
{
"message": "The reservation PDF is not ready.",
"status": "pending",
"retry_after": 5
}
Reload Wallet
POST /api/v1/wallet/reloads
Request by ticket count:
curl -X POST https://safeonward.com/api/v1/wallet/reloads \
-H "Authorization: Bearer $SAFEONWARD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"tickets": 10}'
Request by amount:
{
"amount": 30
}
Response:
{
"checkout_url": "https://checkout.stripe.com/c/pay/cs_test_...",
"tickets": 10
}
The agency must complete Stripe Checkout. Credits are added through the existing Stripe webhook or wallet sync flow.
Error Responses
Missing API key:
{
"message": "Missing API key."
}
Invalid or inactive API key:
{
"message": "Invalid API key."
}
Insufficient credits:
{
"error": {
"code": "insufficient_credits",
"message": "Your prepaid balance is empty. Click here to reload."
},
"reload_url": "https://safeonward.com/api/v1/wallet/reloads"
}
Active duplicate booking:
{
"error": {
"code": "duplicate_active_booking",
"message": "An active booking already exists for this passenger on the same route and departure date."
}
}
Validation errors use Laravel's standard 422 JSON validation response.
Departure dates earlier than the minimum 10-day booking window return a specific 422 error:
{
"error": {
"code": "departure_date_too_soon",
"message": "Departure date must be at least 10 days from today."
},
"minimum_departure_date": "2026-07-03"
}
Reservation document & privacy
White-label. The API returns both a structured JSON order object and a
ready-to-render PDF itinerary (GET /orders/{order_id}/pdf). The PDF is styled
with the operating carrier's logo and name and carries no Safeonward
branding, so you can deliver it to your end-users without re-processing. If
you prefer, you can consume the JSON payload and render your own document.
Data privacy. Passenger data submitted through the API is used solely to create and manage your reservations. It is treated as strictly confidential and is never shared with, marketed to, or used by Safeonward's retail side.
Operational Notes
- Default order wait time:
PUBLIC_API_FULFILLMENT_WAIT_SECONDS=120. - Default polling interval while the request is open:
PUBLIC_API_FULFILLMENT_POLL_MILLISECONDS=500. - Default rate limit:
PUBLIC_API_RATE_LIMIT_PER_MINUTE=60. POST /orderswithtest_mode: truenever consumes a credit and never contacts the booking provider.- Partner order status and PDF routes are scoped to the authenticated agency.
- PDF links are temporary signed URLs and should not be stored permanently by partners unless their integration downloads the file.