> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dev.gojinko.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sandbox walkthrough

> Take a booking all the way to completed in sandbox, paying with a test Shared Payment Token instead of a browser.

This guide walks one flight booking toward `fulfillment.status: "completed"` in the **sandbox** environment, paying **agentically**: no browser, no card, no human at a checkout page.

<Warning>
  **Read this before you start: you cannot currently finish this walkthrough with a sandbox key.**

  Step 4 mints a test payment token, and that endpoint lives on Jinko's **control plane**, which has **no sandbox deployment yet**. There is no sandbox address to send that one request to, so steps 4 through 6 cannot be completed with a sandbox key today.

  Steps 1 to 3 work in sandbox right now, and the request and response shapes documented throughout are accurate. If you need the full path end to end, email [dev@gojinko.com](mailto:dev@gojinko.com) and ask for the current status of the test-token mint. Do not build a sandbox test harness around step 4 until it is available.
</Warning>

Every command below is copy-paste ready. Set two variables and work down the page.

```bash theme={null}
export JINKO_SANDBOX_KEY="jnk_your_sandbox_key"
export JINKO_API="https://api.sandbox.gojinko.com"
```

## What you need

* A **sandbox** API key. Sandbox and production use [separate keys](/authentication/api-keys#sandbox-keys); a production key will not authenticate here.
* `curl` and `jq`.
* For steps 4 to 6, access to the test-token mint, which is [not yet available in sandbox](#4-mint-a-test-shared-payment-token).

You do **not** need a Stripe account, a real card, or a browser.

<Note>
  Sandbox is a fully isolated environment. Bookings made here are never ticketed, never charged, and never reach a real airline.
</Note>

## How agentic payment works

The normal flow sends a human to a checkout page. The agentic flow replaces that step with a **Shared Payment Token** (SPT), a Stripe object that authorizes one specific charge up to one specific amount.

```
checkout → agent_spt_params → mint an SPT scoped to those params → submit → fulfillment
```

In production your buyer's platform mints the SPT. In sandbox you are meant to mint a **test** one yourself, which is what would make this walkthrough self-contained. That mint is the step that is [not yet available](#4-mint-a-test-shared-payment-token).

<Warning>
  **Agentic payment is USD-only.** Stripe runs Shared Payment Tokens on a US account, so a cart quoted in any other currency has no agentic path. Concretely: `checkout` **omits `agent_spt_params`** when the cart isn't USD, and `agent_payment/submit` answers **400 `currency_unsupported`**. Ask for USD explicitly at search time, as every step below does.
</Warning>

## 1) Price a flight

Search mode needs a route and a date. Pass `currency: "USD"` so the cart quotes in USD.

```bash theme={null}
curl -sS -X POST "$JINKO_API/v1/flight_search" \
  -H "X-API-Key: $JINKO_SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "origin": "JFK",
    "destination": "SJC",
    "departure_date": "2026-09-23",
    "trip_type": "oneway",
    "adults": 1,
    "currency": "USD"
  }' | tee search.json | jq -r '.offers[0].fares[0].trip_item_token'
```

Keep that token:

```bash theme={null}
export ITEM_TOKEN=$(jq -r '.offers[0].fares[0].trip_item_token' search.json)
```

## 2) Build the trip

One call creates the trip, adds the item, and attaches the traveler and contact. Both a traveler **and** a contact with `email` and `phone` are required before a trip can be booked.

```bash theme={null}
curl -sS -X POST "$JINKO_API/v1/trip" \
  -H "X-API-Key: $JINKO_SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [{ "trip_item_token": "'"$ITEM_TOKEN"'" }],
    "travelers": [{
      "first_name": "John",
      "last_name": "Doe",
      "date_of_birth": "1990-01-15",
      "gender": "MALE",
      "passenger_type": "ADULT"
    }],
    "contact": { "email": "you@example.com", "phone": "+33600000000" }
  }' | tee trip.json | jq

export TRIP_ID=$(jq -r '.trip_id' trip.json)
```

## 3) Check out and read `agent_spt_params`

`checkout` quotes the cart and returns everything you need to pay for it.

```bash theme={null}
curl -sS -X POST "$JINKO_API/v1/checkout" \
  -H "X-API-Key: $JINKO_SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "trip_id": "'"$TRIP_ID"'" }' | tee checkout.json | jq
```

```json theme={null}
{
  "session_id": "402",
  "checkout_url": "https://app.gojinko.com/checkout?t=<signed token>",
  "status": "ready",
  "expires_at": "2026-09-02T08:55:34Z",
  "payment_type": "agent",
  "total_amount": { "amount": 188.65, "currency": "USD" },
  "items": [ /* … */ ],
  "agent_spt_params": {
    "max_amount": 18865,
    "currency": "USD",
    "expires_at": "2026-09-02T08:55:34Z",
    "stripe_profile": "profile_test_61Uoio…"
  }
}
```

`agent_spt_params` is the mandate for the token you are about to mint. Note `max_amount` is **18865**, not `188.65`: it is in **minor units**, hundredths of a dollar.

```bash theme={null}
export MAX_AMOUNT=$(jq -r '.agent_spt_params.max_amount' checkout.json)
```

<Warning>
  **No `agent_spt_params` in the response?** The cart is not in USD. Re-run step 1 with `"currency": "USD"`. Do not proceed; the mint would produce a token that cannot be redeemed.

  Also check `expires_at`. It is the **quote** deadline, 5 minutes out. If you dawdle between steps, re-run `checkout` before minting. The `checkout_url`'s own 4-hour lifetime is a different clock and does not help you here.
</Warning>

## 4) Mint a test Shared Payment Token

This is the step that replaces your buyer's payment platform: it mints a Stripe **test-mode** token so you can authorize a booking without a real card.

<Warning>
  **Not available in sandbox. This is a gap, not a caveat.**

  This endpoint lives on Jinko's **control plane**, which is a different service from the booking API you have been calling. The control plane has **no sandbox deployment**, so there is no sandbox address for this request and no sandbox host is published for it. The route also does not exist in production, by design, since it mints test-mode tokens.

  The consequence is concrete: **with a sandbox key alone you cannot get a Shared Payment Token today**, and therefore cannot complete steps 5 and 6. Email [dev@gojinko.com](mailto:dev@gojinko.com) to ask where this stands before you plan work around it.

  The contract below is documented so you can write your integration against it now, and so you can recognize the responses when the endpoint becomes reachable.
</Warning>

The request, once you have a host to send it to:

```bash theme={null}
export JINKO_PLATFORM_API="<control-plane host — ask dev@gojinko.com>"

curl -sS -X POST "$JINKO_PLATFORM_API/api/v1/devplatform/sandbox/agent-test-token" \
  -H "X-API-Key: $JINKO_SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "max_amount": '"$MAX_AMOUNT"', "currency": "usd" }' | tee spt.json | jq

export SPT=$(jq -r '.shared_payment_token' spt.json)
```

```json theme={null}
{
  "shared_payment_token": "spt_1Nq8L2eZvKYlo2C0…",
  "max_amount": 18865,
  "currency": "usd",
  "expires_at": "2026-09-02T09:19:34Z"
}
```

| Field                | Rules                                                                                                                                                                           |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `max_amount`         | **Minor units**, 1 to 10,000,000. Pass the `agent_spt_params.max_amount` from step 3 unchanged.                                                                                 |
| `currency`           | Lowercase, and it must be `usd`. Anything else returns **400**. Omitted, it defaults to `usd`.                                                                                  |
| `expires_in_seconds` | Optional. Defaults to 1800, capped at 3600. **A larger value is clamped silently, not rejected**, so don't infer the TTL from what you sent. Read `expires_at` in the response. |

A **503** here means the environment has no Stripe agent account configured. That is a deployment issue, not a bad request. Nothing else returns 503.

## 5) Submit the payment

<Note>
  This step needs the `$SPT` from step 4, so in sandbox it is **not reachable today**. The contract is documented so you can build against it.
</Note>

Hand the trip and the token to Jinko. This authorizes the charge and schedules fulfillment in one call.

```bash theme={null}
curl -sS -X POST "$JINKO_API/v1/agent_payment/submit" \
  -H "X-API-Key: $JINKO_SANDBOX_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "trip_id": "'"$TRIP_ID"'",
    "shared_payment_token": "'"$SPT"'"
  }' | jq
```

```json theme={null}
{
  "fulfillment_cart_id": 913,
  "status": "processing",
  "payment_verified": true
}
```

<Note>
  **If the response carries a `checkout_url`, the agentic path stopped.** The card issuer asked for a 3DS step-up, or declined. Send a human to that URL to finish. This is the designed fallback, not an error, so handle it in production code.
</Note>

## 6) Poll until it lands

`get_trip` is a cheap read rather than a live provider call, so polling every few seconds is fine.

```bash theme={null}
while true; do
  status=$(curl -sS "$JINKO_API/v1/trip/$TRIP_ID" \
    -H "X-API-Key: $JINKO_SANDBOX_KEY" \
    | jq -r '.fulfillment.status // "awaiting_payment"')
  echo "Status: $status"
  case "$status" in
    completed|partial|failed|cancelled|expired_quote|exchange_partial_failure) break ;;
  esac
  sleep 5
done

curl -sS "$JINKO_API/v1/trip/$TRIP_ID" \
  -H "X-API-Key: $JINKO_SANDBOX_KEY" | jq '.bookings'
```

At `completed` you get one entry per item:

```json theme={null}
[
  {
    "item_id": "prod_f5pyznimu4",
    "kind": "flight",
    "booking_reference": "JNK-8PT9VS",
    "pnr": "XM9L2K",
    "provider_status": "CONFIRMED"
  }
]
```

That is a full agentic booking, once step 4 is reachable. See the [fulfillment table](/guides/flight-booking#7-fulfillment-is-automatic) for every state this loop can end on, and remember that `partial` means some items booked and some did not.

<Note>
  The polling loop itself works in sandbox today against any trip, including one paid through the ordinary [hosted checkout](/guides/flight-booking#5-checkout). Only the token mint in step 4 is blocked.
</Note>

## When something goes wrong

| Symptom                               | Cause                                                                                       | Fix                                                                                                                                               |
| ------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401` on any call                     | Production key against sandbox, or the reverse                                              | Keys are environment-scoped. Mint a sandbox key in the dashboard.                                                                                 |
| No `agent_spt_params` on `checkout`   | Cart isn't USD                                                                              | Re-search with `"currency": "USD"`.                                                                                                               |
| `400 CURRENCY_UNSUPPORTED` on submit  | Same, caught later                                                                          | As above.                                                                                                                                         |
| `400` on the mint                     | `currency` wasn't `usd`, or `max_amount` was out of range                                   | `max_amount` is minor units, 1 to 10,000,000.                                                                                                     |
| `503` on the mint                     | No Stripe agent account in that environment                                                 | Email [dev@gojinko.com](mailto:dev@gojinko.com).                                                                                                  |
| `404` on the mint                     | The route is non-production only, by design, and there is no sandbox control plane to reach | Not solvable from your side today. See [step 4](#4-mint-a-test-shared-payment-token).                                                             |
| `checkout_url` on the submit response | 3DS step-up or decline                                                                      | Expected fallback. Send a human to the URL.                                                                                                       |
| `410 QUOTE_EXPIRED` on submit         | The 5-minute `expires_at` elapsed between checkout and submit                               | Nothing was charged and no payment object exists. Re-run `checkout` (this re-quotes), then mint a fresh token against the new `agent_spt_params`. |

## What's next?

* [Webhooks](/guides/webhooks) so you learn a booking landed without polling.
* [Flight booking guide](/guides/flight-booking) for the browser-based checkout flow.
* [Errors](/concepts/errors) for the full status-code reference, including what happens when a price moves.
