Safeonward Public B2B API v1
The public B2B API lets an approved agency create onward ticket reservation PDFs from its prepaid wallet.
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",
"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
}
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.
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"
}
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"
}
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.