Bootstrap an embed token
POST /embed/bootstrap - Exchange a publishable key and optional client secret for a short-lived embed token.
0Gate is the primary public integration path for hosted payment, ramp, and swap experiences. Keep secret-key operations on your server and hand only browser-safe values to the widget.
Bootstrap is the one and only call the browser makes with a publishable key. It exchanges the pk_* (plus an optional session clientSecret) for a short-lived embed token (a JWT) that the iframe then sends as X-Embed-Token on every subsequent request. Everything else in 0Gate runs server-side with your secret key.
Endpoint
| Field | Value |
|---|---|
| Method | POST |
| Path | /v1/embed/bootstrap |
| Area | Embed |
| Operation id | createEmbedToken |
| Auth boundary | Publishable key from an allowed browser origin. |
Use it for
Exchange a publishable key and optional client secret for a short-lived embed token.
Use this endpoint only for the partner-scoped resource it describes. Store your own reference id, the returned 0Bit object id, the request id, timestamps, and the current status so support and reconciliation do not depend on browser callbacks alone.
Authentication
This is the only public endpoint authenticated with a publishable key (pk_test_* / pk_live_*). Publishable keys are browser-safe and may only call bootstrap; every other 0Gate endpoint requires a secret key (sk_*) and must run on your server.
pk_* vs sk_*
Never ship a secret key (sk_*) to the browser. The publishable key is the only credential that belongs in front-end code, and it can do nothing except bootstrap an embed token.
The publishable key is accepted from any of three locations, checked in this order:
| Source | Example | Use it for |
|---|---|---|
Authorization header | Authorization: Bearer pk_test_… | The default; what the widget.js SDK sends. |
X-Publishable-Key header | X-Publishable-Key: pk_test_… | When the Authorization slot is used for something else. |
publishable_key query | ?publishable_key=pk_test_… | Last-resort for static embeds that cannot set headers. |
The request Origin (or Referer fallback) must be an exact match for one of your configured allowed domains — no wildcards. A browser caller with no Origin/Referer is refused.
Production rules
- Keep secret keys on your server. Bootstrap uses the publishable key only.
- Register every embedding origin in your allowed domains before going live — an unlisted origin is rejected.
- Pass the session
clientSecretwhenever you create a session server-side, so the embed token is bound to that session's locked amount and currency. - Branch on machine-readable status, error code, object id, and request id.
- Treat examples and placeholder ids as fake data only.
Request body
The body is optional. A bare bootstrap (no body) mints a session-less token for partners not yet using sessions.
| Field | Required | Type | Use it for |
|---|---|---|---|
clientSecret | No | string | Binds the embed token to a session created with POST /gate_sessions. When present, downstream buy/sell calls enforce the session's locked amount and currency. |
Casing gotcha: clientSecret vs client_secret
The session is returned by POST /gate_sessions with the field named client_secret (snake_case). When you pass it back here, the body field is clientSecret (camelCase). Read snake, send camel.
An invalid, expired, already-consumed, or cross-partner/cross-mode clientSecret is rejected with 403.
Response
The response is HTTP 200 with the embed token and the context the iframe needs to render. When the call is session-bound, the locked session details are echoed back so the iframe can show the right amount and flow.
| Field | When present | Type | Use it for |
|---|---|---|---|
embed_token | Always | string | The short-lived JWT. Send it as the X-Embed-Token header on every subsequent iframe call. Empty string ("") when available is false. |
expires_at | Always | string (ISO 8601) | When the embed token expires. Refresh before this with POST /embed/refresh. |
partner_id | Always | string | Your partner id, echoed for logging and reconciliation. |
mode | Always | test/live | The mode of the publishable key used. |
session_id | Always | string | null | The bound session id when a clientSecret was supplied; otherwise null. |
amount | Session-bound | string | null | The locked fiat amount echoed from the session, as a decimal string. |
currency | Session-bound | string | null | The locked currency echoed from the session. |
target_token | Session-bound | string | null | The locked target asset echoed from the session. |
target_network | Session-bound | string | null | The locked delivery network echoed from the session. |
return_url | Session-bound | string | null | The post-checkout return URL echoed from the session. |
flow | Session-bound | string | null | The locked flow (on_ramp, off_ramp, or swap) echoed from the session. |
available | Always | boolean | true for a usable token; false when a live partner is over quota (see below). |
unavailable_reason | When available: false | string | null | Machine-readable reason the bootstrap is unavailable; null otherwise. |
frame_ancestors_policy | Always (bootstrap) | string | The frame-ancestors policy derived from your allowed domains, so the iframe is framed only by approved origins. |
theme | Always | object | null | Your iframe co-branding tokens, or null for the default Gate branding. |
Over-quota is a 200, not an error
When a live partner is over quota, bootstrap still returns HTTP 200 with available: false, a populated unavailable_reason, and an empty embed_token (""). This lets the SDK render a neutral unavailable state instead of throwing on a rejected fetch. Always branch on available before using embed_token. Test mode is unmetered and always available.
Examples
curl -X POST https://gate-api.0bit.app/v1/embed/bootstrap \
-H "Authorization: Bearer pk_test_xxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Origin: https://app.example.com" \
-H "Content-Type: application/json" \
-d '{
"clientSecret": "gsec_test_67a1f3b9000000000000_AbCdEfPlaceholder0123456789"
}'The same key is also accepted as -H "X-Publishable-Key: pk_test_…" or as a ?publishable_key=pk_test_… query parameter.
{
"embed_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVC9.test.placeholder",
"expires_at": "2026-01-01T01:00:00Z",
"partner_id": "ptnr_test_000000000123",
"mode": "test",
"session_id": "gs_test_000000000456",
"amount": "100.00",
"currency": "EUR",
"target_token": "USDT",
"target_network": "arbitrum",
"return_url": "https://app.example.com/checkout/return",
"flow": "on_ramp",
"available": true,
"unavailable_reason": null,
"frame_ancestors_policy": "frame-ancestors https://app.example.com",
"theme": null
}Hand embed_token to the iframe and send it as X-Embed-Token on subsequent calls. A session-less bootstrap (no clientSecret) returns the same shape with session_id and the session-echo fields all null.
Errors
All errors use the unified envelope and carry an X-Request-Id response header. Branch on code/type/statusCode, not on the free-form message.
{
"type": "forbidden",
"code": "forbidden",
"message": "Example credential error using fake data.",
"request_id": "req_test_000000000123",
"doc_url": null,
"statusCode": 403
}| Status | type | When it happens |
|---|---|---|
403 | forbidden | Missing or invalid publishable key; key is not a publishable key; origin not in your allowed domains; missing Origin/Referer; or an invalid, expired, already-consumed, or cross-partner/cross-mode clientSecret. |
429 | rate_limited | Throttled at 30 requests / minute / IP. Back off and retry. |
5xx | server_error | Transient server or upstream failure. Retry with bounded backoff. |
Credential failures are 403, not 401
A bad, missing, or wrong-type publishable key is rejected with 403 — the endpoint deliberately does not distinguish "key missing" from "key invalid" to avoid leaking which keys exist.
Over quota is not an error
A live partner that is over quota returns 200 with available: false and an empty embed_token, not a 4xx. Reserve error handling for the statuses above.
Public boundary
This reference covers partner-scoped endpoint behavior, authentication, idempotency, webhook verification, and support-safe records. Internal operations, administrative routes, and unsupported availability claims are outside the public API contract.