Error philosophy

Errors in the API are designed to be immediately actionable — every failure response tells you exactly what went wrong and what to do next. Three principles guide the design: 1. The error code is stable, the message is human. Every error body contains a machine-readable error code that is stable across API versions. Use this in your conditionals. The message field is plain English, safe to log or display, but not for programmatic branching. 2. The status code maps to a category, the body maps to a cause. 402 always means insufficient balance. 429 always means you’re rate-limited. 422 means the request was syntactically valid but semantically wrong. The body narrows it further with a specific error code and details where relevant. 3. Opaque errors are a bug. A 403 that gives you nothing to act on, or a 500 with no context, is a platform defect. Every error response should either tell you how to fix the request or tell you to retry with specific timing guidance.
{
  "error": "insufficient_balance",
  "message": "Free balance is below the required hold amount for this send.",
  "details": [],
  "required_amount": 0.12,
  "topup_url": "https://app.arowana.app/billing"
}
Read the full Errors reference for all status codes, the envelope shape, and the 402 balance response fields.

Billing on success

Successful sends return a billing object inline — no separate call needed to see your balance, hold, tier position, or message classification.
{
  "message_id": "msg_a1b2c3d4",
  "status": "queued",
  "created_at": "2026-03-27T14:30:00Z",
  "billing": {
    "channel": "RCS",
    "message_type": "single_message",
    "hold_amount": 0.12,
    "message_price": 0.10,
    "balance": {
      "free": 40.88,
      "reserved": 39.12,
      "total": 80.00
    },
    "tier": {
      "channel": "RCS",
      "current": "tier_1",
      "volume_used": 12501
    }
  }
}
FieldMeaning
hold_amountFunds reserved for this in-flight message
message_priceExpected final cost once delivery is confirmed
balance.freeImmediately spendable — what the 402 check runs against
balance.reservedCurrently held across all in-flight messages
tier.volume_usedMessages sent this month toward the next tier threshold

Common HTTP errors

StatusMeaningWhat you get
400Validation failuredetails[] with field-level errors
402Insufficient balancerequired_amount, topup_url
403Policy violation or agent not approvedAgent status or generic test error
404Unknown resource or device not RCS-capableContext fields per route
422Semantic validation failureDomain-specific reason in error
429Rate limitedretry_after
Test Agent sends return a generic 403 when the target number is not the verified test phone. The response deliberately does not reveal whether the number belongs to another account.

Idempotency

Send endpoints accept an idempotency key so safe retries never duplicate a message. Pass the key in the Idempotency-Key request header. Use a UUID generated per logical send attempt — retry the same request with the same key if it fails.
curl -X POST "https://api.arowana.app/v1/messages" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{ ... }'
If the original request succeeded but the response was lost in transit, replaying with the same key returns the original response — no duplicate charge, no duplicate send.

Errors reference

Full status code table, error envelope shape, and 402 balance response fields.

Billing API

Proactive balance checks and tier queries before large sends.

API conventions

Rate limits, retry patterns, and idempotency details.

Our philosophy

Why errors and billing are designed to be transparent and actionable.