Skip to content

Getting started

Errors

Failures use conventional HTTP status codes and always return the same JSON shape, with a stable code to switch on and a message you can show to people.

The error object

Every failed request returns this shape with a matching HTTP status. code never changes once published; message may be reworded. details is present when there is more to say.

402 Payment Required
{
  "error": {
    "code": "insufficient_balance",
    "message": "Your balance is too low for this purchase. Add funds and try again.",
    "details": {
      "balance": "0.05",
      "balance_micros": 50000,
      "required": "0.895",
      "required_micros": 895000
    }
  }
}

HTTP status codes

200 · 201
Success. 201 when something was created.
400
The request is malformed or a parameter is invalid.
401
The API key is missing or invalid.
402
Your balance doesn't cover the purchase.
403
Not allowed for this account right now.
404
The resource or endpoint doesn't exist (or isn't yours).
405
The endpoint exists but not with that method. The Allow header lists the ones it takes.
409
Conflicts with the current state: no stock, over your max price, wrong status, reused key.
413
The request body is too large.
429
Rate limited. Wait for Retry-After.
5xx
Something failed on our side or upstream. Safe to retry with the same Idempotency-Key.

Error codes

Every code the API returns. Retryable ones can succeed later without changing the request.

  • invalid_requestHTTP status 400A parameter is missing, malformed or out of range. details.fields names each one.no — not retryable
  • unauthorizedHTTP status 401The API key is missing, malformed or revoked.no — not retryable
  • insufficient_balanceHTTP status 402Your balance doesn't cover the price. details has balance_micros and required_micros.no — not retryable
  • account_disabledHTTP status 403The API key is valid but its account is disabled. Contact support@passcode.sh.no — not retryable
  • not_availableHTTP status 403Real carrier numbers aren't on sale to your account yet. Demo inventory still works.no — not retryable
  • country_not_foundHTTP status 404We don't sell numbers in that country. List them with GET /countries.no — not retryable
  • not_foundHTTP status 404No endpoint has that path.no — not retryable
  • order_not_foundHTTP status 404No order with that id belongs to your account.no — not retryable
  • rental_not_foundHTTP status 404No rental with that id belongs to your account.no — not retryable
  • service_not_foundHTTP status 404No enabled service has that slug. List them with GET /services.no — not retryable
  • user_not_foundHTTP status 404The account no longer exists.no — not retryable
  • method_not_allowedHTTP status 405The path exists but doesn't take that HTTP method, e.g. GET on a cancel endpoint. The Allow header and details.allowed list the methods it does take.no — not retryable
  • idempotency_conflictHTTP status 409The Idempotency-Key was already used with a different body. details.order_id (or details.rental_id) names what it created.no — not retryable
  • invalid_stateHTTP status 409The order or rental can't make that transition, e.g. cancelling after a code arrived. details.status is the current status.no — not retryable
  • no_inventoryHTTP status 409Nothing is in stock for that service and country right now. If an order had already been charged, it was refunded and details.order_id names it.retry — retryable
  • price_above_maxHTTP status 409The best available price is above your max_price. details.price_micros is the lowest price on offer.retry — retryable
  • payload_too_largeHTTP status 413The JSON body is larger than 16 KB.no — not retryable
  • rate_limitedHTTP status 429Too many requests. Wait for Retry-After seconds.retry — retryable
  • internal_errorHTTP status 500Something failed on our side. Retry with the same Idempotency-Key; contact support with the X-Request-Id if it persists.retry — retryable
  • provider_errorHTTP status 502The upstream carrier failed. Safe to retry.retry — retryable

Handling errors

Branch on error.code, not on the message or the status alone — several codes share a status.

const res = await fetch("https://passcode.sh/api/v1/orders", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PASSCODE_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({ service: "whatsapp", country: "US", max_price: "1.00" }),
});

if (!res.ok) {
  const { error } = await res.json();
  switch (error.code) {
    case "insufficient_balance":
      // Top up, then retry with the same Idempotency-Key.
      break;
    case "no_inventory":
    case "price_above_max":
      // Try another country, or raise max_price.
      break;
    case "rate_limited":
      // Wait Retry-After seconds.
      break;
    default:
      throw new Error(`${error.code}: ${error.message} (${res.headers.get("x-request-id")})`);
  }
}

Request IDs

Every response carries an X-Request-Id header (req_…). Log it with failures and include it when you email support@passcode.sh — it lets us find the exact request.