Everything on this page is plain JSON over HTTPS, end to end. There is no manual step: signup returns a verification code you redeem over the API, so a human, a CI job, or an AI agent can go from a fresh email address to a first testnet transaction without a mailbox or a browser. This sequence is validated against staging.
Staging is isolated by construction: it only ever touches testnets, and the sandbox credentials issued here carry a fixed, least-privilege scope set. Production credentials are not self-serve.
The sequence
1. Sign up
curl -X POST https://core-staging.coinbax.com/api/v1/auth/signup \
-H "Content-Type: application/json" \
-d '{
"email": "dev@example.com",
"password": "<strong password: 8+ chars, upper, lower, number, special>",
"firstName": "Dev",
"lastName": "Eloper",
"companyName": "Example Co",
"intendedUseCase": "escrow"
}'
email, password, firstName and lastName are required. companyName
and intendedUseCase are optional here, because the browser flow collects
them after login, but sending them is recommended: companyName names the
workspace that is created for you, and without it the name falls back to
the local part of your email address. intendedUseCase is one of marketplace,
freelance, escrow, b2b, saas, other. On staging, accounts are
approved automatically and a workspace is created for you. The response
includes both the workspace id and, on staging only, a
verificationCode you use in the next step:
{
"success": true,
"data": {
"user": { "id": "...", "email": "dev@example.com" },
"workspace": { "id": "...", "name": "Example Co", "status": "active" },
"verificationCode": "a1b2c3..."
}
}
export WORKSPACE_ID=... # data.workspace.id
export VERIFICATION_CODE=... # data.verificationCode (staging only)
Production never returns verificationCode; there, verification is the
emailed link. A verification email is always sent on staging too, so the
link still works if you prefer it.
If the email already has a Coinbax account, signup returns
409 EMAIL_HAS_IDENTITY with a loginUrl instead. Sign in rather than
signing up again; do not retry the signup.
Signup is rate-limited to 5 accounts per hour per IP address, and 20 per
hour across a /24 (or a /48 on IPv6). Exceeding either returns the standard
429 envelope with Retry-After and X-RateLimit-* headers. This is
generous for real automation, since a CI job or an agent needs one account
rather than many, and it exists because signup is an unauthenticated
write. If you need more accounts than that for a legitimate test matrix,
talk to us rather than working around the limit: reuse one account and
issue additional sandbox credentials instead.
2. Verify your email
Redeem the code from step 1. This runs the same verification the emailed link would, and provisions your identity so you can sign in:
curl -X POST https://core-staging.coinbax.com/api/v1/auth/verify-code \
-H "Content-Type: application/json" \
-d "{ \"email\": \"dev@example.com\", \"code\": \"$VERIFICATION_CODE\" }"
A 200 means you are verified and can sign in. The code is single-use and
expires; if verification transiently fails, the code is preserved so you can
safely retry the same request.
3. Sign in
Sign in through the identity endpoint. It returns an access token (or an MFA challenge, which self-serve accounts do not have enrolled by default).
curl -X POST https://core-staging.coinbax.com/api/v1/identity/login \
-H "Content-Type: application/json" \
-d '{ "email": "dev@example.com", "password": "..." }'
The response includes your access token; the workspace id was returned at signup (step 1). Export both:
export WORKSPACE_JWT=... # data.accessToken from the login response
export WORKSPACE_ID=... # data.workspace.id from the signup response
This JWT authenticates you against the Workspace API
(core-staging.coinbax.com) — workspace administration, including issuing the
credentials in the next step. It is not the token you use to move money; that
one comes from step 5.
4. Issue sandbox credentials
curl -X POST "https://core-staging.coinbax.com/api/v1/workspaces/$WORKSPACE_ID/sandbox-credentials" \
-H "Authorization: Bearer $WORKSPACE_JWT"
One call returns two credentials, each exactly once:
{
"success": true,
"data": {
"platformId": "...",
"name": "sandbox-example-co",
"apiKey": "cbx_...",
"oauthClient": {
"clientId": "coinbax_client_...",
"clientSecret": "...",
"scopes": ["read:transactions", "write:transactions", "..."]
},
"scopes": ["read:transactions", "write:transactions", "..."],
"createdAt": "..."
}
}
apiKey is always returned; oauthClient is conditional. Use oauthClient
for the Payments API v2, which accepts OAuth 2.0 Bearer tokens only. The
apiKey is for /api/v1, which is deprecated and sunsets on
1 November 2026.
Store what you were given immediately — neither the key nor the client secret
can be retrieved again. Read oauthClient defensively rather than assuming it
is present:
export COINBAX_CLIENT_ID=coinbax_client_...
export COINBAX_CLIENT_SECRET=...
Limits: 2 active sandbox credentials per workspace (revoke with
DELETE .../sandbox-credentials/{platformId}, then issue another), and
issuance is rate limited to 5 per hour.
If oauthClient is absent, the client could not be created and the warning
field says so. That credential works on /api/v1 only — revoke it and issue
another rather than falling back to v1.
5. Exchange the client credentials for an access token
get_payments_token() {
local response
# The body goes in on stdin (-d @-), never as an argument: a secret in the
# argument list is readable by any local process via `ps` or /proc.
response=$(jq -n \
--arg id "$COINBAX_CLIENT_ID" \
--arg secret "$COINBAX_CLIENT_SECRET" \
'{grant_type: "client_credentials", client_id: $id, client_secret: $secret}' \
| curl -sS --fail-with-body -X POST \
https://api-staging.coinbax.com/api/v2/oauth/token \
-H "Content-Type: application/json" \
-d @-) || { echo "token exchange failed" >&2; return 1; }
# Only the parsed field is echoed. Never print the raw response on failure:
# a successful body contains the bearer token, and terminal or CI logs keep it.
printf '%s' "$response" | jq -er '.data.access_token' \
|| { echo "token exchange returned no access_token" >&2; return 1; }
}
PAYMENTS_TOKEN=$(get_payments_token) && export PAYMENTS_TOKEN
Three things this guards, all of which have bitten people:
- The secret never reaches the argument list.
jq -nbuilds the JSON andcurl -d @-reads it from stdin, sopscannot see it. - A failed exchange fails loudly.
--fail-with-bodystops curl swallowing an HTTP error, andjq -eexits non-zero on a missing or null field, so you never export an empty token and debug a confusing 401 later. - Nothing logs the response body. On success it contains the bearer token.
This endpoint uses RFC 6749’s snake_case field names, unlike the rest of the
API: grant_type, client_id, client_secret, and access_token in the
response. Tokens expire in an hour (expires_in, in seconds), so request a
fresh one rather than caching it indefinitely. An optional scope parameter
narrows a token below the client’s granted set; it can never widen it.
6. Create your first transaction
A transaction moves funds between two wallet addresses. fromAddress,
toAddress, and amount are the only required fields, so you can create your
first payment immediately. orchestrationType raw is a direct transfer;
coinbax routes through programmable escrow and requires a templateId with
a deployed contract for your workspace.
curl -X POST https://api-staging.coinbax.com/api/v2/transactions \
-H "Authorization: Bearer $PAYMENTS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"fromAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb2",
"toAddress": "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
"amount": 100,
"currency": "USDC",
"blockchainNetwork": "base",
"orchestrationType": "raw",
"idempotencyKey": "your-unique-key"
}'
The response returns the transaction in pending; poll
GET /api/v2/transactions/{id} (or subscribe a webhook) to watch it move
through the lifecycle.
Customer records are optional. They live on the Workspace API
(/workspaces/{id}/customers) and are managed by workspace admins, not
required to create a transaction. Pass customerId on a transaction only if
you have created one.
What sandbox credentials can and cannot do
Both credentials carry the same fixed scope set, chosen server-side and never taken from the request:
| transactions | read, write, move, reverse, cancel, rescind |
| customers | read, write |
| webhooks | read, write |
| templates | read |
| platform | read |
move:transactions and reverse:transactions are the v2 money-moving scopes:
move: covers the four release routes, reverse: covers refunds,
cancellations, rescissions, and batch cancel. v2 splits money movement out of
write:transactions, so a token holding only write: can create a
transaction but not release or reverse one. cancel: and rescind: are the
v1 equivalents, included so the API key still works against /api/v1 until
it sunsets. They are not accepted on v2 and are no longer offered to new
OAuth clients; the sandbox is the exception because it issues both a v1 key
and a v2 client from one call.
Excluded by design: platform administration, credential minting, template authoring, compliance triggers, and dispute operations.
Sandbox platforms also carry write quotas on staging: 200 transactions per
day, 100 customers, and 5 webhook subscriptions per sandbox platform. Hitting
a quota returns a standard 429 envelope with Retry-After.
Notes for AI agents
- This site’s MCP server exposes this sequence via the
getting_startedtool, plus endpoint schemas viaget_endpoint. - Every request and response above follows the unified
success / data / meta / errorenvelope. - The whole flow is headless on staging: read
verificationCodefrom the signup response and POST it to/auth/verify-code. No mailbox needed. - On signup
409 EMAIL_HAS_IDENTITY, switch to/identity/loginwith the same credentials; do not retry signup. - On signup
429, honourRetry-Afterand back off. Do not rotate IP addresses orX-Forwarded-Forto evade the limit; the limit is not read from client-supplied headers, and the attempt will simply fail. - Do not retry sandbox credential issuance on
409; revoke an existing credential first (2 active max per workspace). - Two distinct tokens are in play:
WORKSPACE_JWTauthenticates the Workspace API (core-staging),PAYMENTS_TOKENauthenticates the Payments API v2 (api-staging). Sending the wrong one is a 401, not a 403. - The token endpoint is the one place that uses snake_case
(
grant_type/client_id/client_secret,access_token). Everything else is camelCase. - Credentials are staging-only. There is no API path from here to production credentials; that is a conversation with the Coinbax team.