Introduction
SOMA enables shared payment for a single order. Merchants integrate SOMA at checkout to allow customers to split payments between multiple participants.
Welcome to SOMA API
SOMA is a B2B SaaS module that enables shared payment for a single order. Integrate SOMA at checkout to offer your customers the ability to split payments between multiple participants.
How it works
- Create a Setup - When a customer chooses "Pay with SOMA", create a setup via API
- Share the link - The primary buyer receives a link to configure the payment split
- Participants pay - Each participant pays their share via their unique payment link
- Order completed - When 100% is collected, SOMA notifies you via webhook
Key concepts
- Setup: A payment collection session for a single order
- Participant: Someone who pays a portion of the order
- Share URL: A unique link for each participant to pay their share
Authenticating requests
To authenticate requests, include an Authorization header with the value "Bearer soma_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx".
All authenticated endpoints are marked with a requires authentication badge in the documentation below.
To authenticate, include your API key in the Authorization header as a Bearer token:
Authorization: Bearer soma_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
API keys are provided when you register as a merchant. Contact support if you need to regenerate your key.
Setups
APIs for managing payment setups (SOMA sessions).
A Setup represents a payment collection session for a single order. When a customer chooses to pay with SOMA, you create a setup via this API. The setup generates shareable URLs that allow the primary buyer to configure the payment split and participants to pay their shares.
List setups
requires authentication
Retrieve a paginated list of setups for your merchant account. Results are ordered by creation date (newest first).
Example request:
curl --request GET \
--get "http://soma.localhost/api/v1/somas?status=collecting&order_id=order_12345&per_page=50" \
--header "Authorization: Bearer soma_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://soma.localhost/api/v1/somas"
);
const params = {
"status": "collecting",
"order_id": "order_12345",
"per_page": "50",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer soma_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'http://soma.localhost/api/v1/somas';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer soma_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'status' => 'collecting',
'order_id' => 'order_12345',
'per_page' => '50',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Success):
{
"data": [
{
"id": "01HQXYZ123456789ABCDEFGH",
"status": "collecting",
"order_id": "order_12345",
"amount_total_cents": 15000,
"currency": "EUR",
"paid_amount_cents": 7500,
"progress_percentage": 50,
"expires_at": "2024-01-16T12:00:00+00:00",
"completed_at": null,
"created_at": "2024-01-15T12:00:00+00:00"
},
{
"id": "01HQABC987654321ZYXWVUTS",
"status": "completed",
"order_id": "order_12340",
"amount_total_cents": 5000,
"currency": "EUR",
"paid_amount_cents": 5000,
"progress_percentage": 100,
"expires_at": "2024-01-15T12:00:00+00:00",
"completed_at": "2024-01-14T18:30:00+00:00",
"created_at": "2024-01-14T12:00:00+00:00"
}
],
"meta": {
"current_page": 1,
"last_page": 5,
"per_page": 20,
"total": 98
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create a setup
requires authentication
Create a new payment setup (SOMA session) for an order. This endpoint is idempotent -
if you send the same idempotency_key, you'll get the same setup back.
When a setup is created, it starts in draft status. Share the share_setup_url with
your customer so they can configure how to split the payment among participants.
Example request:
curl --request POST \
"http://soma.localhost/api/v1/somas" \
--header "Authorization: Bearer soma_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"order_id\": \"order_12345\",
\"amount_total_cents\": 15000,
\"currency\": \"EUR\",
\"success_url\": \"https:\\/\\/your-store.com\\/orders\\/12345\\/success\",
\"cancel_url\": \"https:\\/\\/your-store.com\\/orders\\/12345\\/cancel\",
\"idempotency_key\": \"order_12345_soma_v1\",
\"metadata\": {
\"customer_email\": \"john@example.com\",
\"plan\": \"premium\"
},
\"expiration_minutes\": 1440
}"
const url = new URL(
"http://soma.localhost/api/v1/somas"
);
const headers = {
"Authorization": "Bearer soma_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"order_id": "order_12345",
"amount_total_cents": 15000,
"currency": "EUR",
"success_url": "https:\/\/your-store.com\/orders\/12345\/success",
"cancel_url": "https:\/\/your-store.com\/orders\/12345\/cancel",
"idempotency_key": "order_12345_soma_v1",
"metadata": {
"customer_email": "john@example.com",
"plan": "premium"
},
"expiration_minutes": 1440
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'http://soma.localhost/api/v1/somas';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer soma_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'order_id' => 'order_12345',
'amount_total_cents' => 15000,
'currency' => 'EUR',
'success_url' => 'https://your-store.com/orders/12345/success',
'cancel_url' => 'https://your-store.com/orders/12345/cancel',
'idempotency_key' => 'order_12345_soma_v1',
'metadata' => [
'customer_email' => 'john@example.com',
'plan' => 'premium',
],
'expiration_minutes' => 1440,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Idempotent retry):
{
"id": "01HQXYZ123456789ABCDEFGH",
"status": "draft",
"share_setup_url": "https://somapay.co/s/abc123xyz/setup",
"payment_url": "https://somapay.co/p/abc123xyz",
"order_id": "order_12345",
"amount_total_cents": 15000,
"currency": "EUR",
"expires_at": "2024-01-16T12:00:00+00:00",
"created_at": "2024-01-15T12:00:00+00:00",
"_idempotent": true
}
Example response (201, Setup created):
{
"id": "01HQXYZ123456789ABCDEFGH",
"status": "draft",
"share_setup_url": "https://somapay.co/s/abc123xyz/setup",
"payment_url": "https://somapay.co/p/abc123xyz",
"order_id": "order_12345",
"amount_total_cents": 15000,
"currency": "EUR",
"expires_at": "2024-01-16T12:00:00+00:00",
"created_at": "2024-01-15T12:00:00+00:00"
}
Example response (409, Duplicate order):
{
"error": "duplicate_order",
"message": "An active setup already exists for order_id: order_12345",
"existing_setup_id": "01HQXYZ123456789ABCDEFGH",
"existing_setup_status": "collecting"
}
Example response (422, Invalid currency):
{
"error": "invalid_currency",
"message": "Currency USD is not allowed for this merchant.",
"allowed_currencies": [
"EUR",
"GBP"
]
}
Example response (422, Validation error):
{
"error": "validation_error",
"message": "The given data was invalid.",
"errors": {
"amount_total_cents": [
"The amount must be an integer (in cents)."
],
"idempotency_key": [
"The idempotency_key field is required for safe retries."
]
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Get a setup
requires authentication
Retrieve details of a specific setup including its participants and payment progress.
Example request:
curl --request GET \
--get "http://soma.localhost/api/v1/somas/01HQXYZ123456789ABCDEFGH" \
--header "Authorization: Bearer soma_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://soma.localhost/api/v1/somas/01HQXYZ123456789ABCDEFGH"
);
const headers = {
"Authorization": "Bearer soma_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'http://soma.localhost/api/v1/somas/01HQXYZ123456789ABCDEFGH';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer soma_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Success):
{
"id": "01HQXYZ123456789ABCDEFGH",
"status": "collecting",
"order_id": "order_12345",
"amount_total_cents": 15000,
"currency": "EUR",
"paid_amount_cents": 7500,
"remaining_amount_cents": 7500,
"progress_percentage": 50,
"share_setup_url": "https://somapay.co/s/abc123xyz/setup",
"payment_url": "https://somapay.co/p/abc123xyz",
"success_url": "https://your-store.com/orders/12345/success",
"cancel_url": "https://your-store.com/orders/12345/cancel",
"metadata": {
"customer_email": "john@example.com"
},
"expires_at": "2024-01-16T12:00:00+00:00",
"completed_at": null,
"created_at": "2024-01-15T12:00:00+00:00",
"updated_at": "2024-01-15T14:30:00+00:00",
"participants": [
{
"id": "01HQPART123456789ABCDEFG",
"label": "Alice",
"amount_cents": 7500,
"status": "paid",
"paid_at": "2024-01-15T14:30:00+00:00"
},
{
"id": "01HQPART987654321GFEDCBA",
"label": "Bob",
"amount_cents": 7500,
"status": "pending",
"paid_at": null
}
]
}
Example response (404, Not found):
{
"error": "not_found",
"message": "Setup not found."
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Cancel a setup
requires authentication
Cancel a setup that is still in draft or collecting status, provided no payments have been made.
Once canceled, the setup cannot be reactivated - create a new one instead.
Example request:
curl --request POST \
"http://soma.localhost/api/v1/somas/01HQXYZ123456789ABCDEFGH/cancel" \
--header "Authorization: Bearer soma_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"http://soma.localhost/api/v1/somas/01HQXYZ123456789ABCDEFGH/cancel"
);
const headers = {
"Authorization": "Bearer soma_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "POST",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'http://soma.localhost/api/v1/somas/01HQXYZ123456789ABCDEFGH/cancel';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer soma_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Success):
{
"id": "01HQXYZ123456789ABCDEFGH",
"status": "canceled",
"message": "Setup has been canceled."
}
Example response (404, Not found):
{
"error": "not_found",
"message": "Setup not found."
}
Example response (422, Invalid state):
{
"error": "invalid_state",
"message": "Cannot cancel a setup with status: completed"
}
Example response (422, Has payments):
{
"error": "has_payments",
"message": "Cannot cancel a setup that has received payments. Please contact support for refunds.",
"paid_amount_cents": 7500
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Webhooks
SOMA sends webhooks to notify your application about important events. Configure your webhook endpoint URL in your merchant dashboard.
Webhook Security
All webhooks include a signature header for verification:
X-Soma-Signature: t=1705320000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
The signature consists of:
t: Unix timestamp when the webhook was sentv1: HMAC-SHA256 signature of{timestamp}.{payload}
Verifying Signatures (PHP)
function verifyWebhookSignature(string $payload, string $signatureHeader, string $secret): bool
{
$parts = [];
foreach (explode(',', $signatureHeader) as $part) {
[$key, $value] = explode('=', $part, 2);
$parts[$key] = $value;
}
$timestamp = $parts['t'] ?? '';
$signature = $parts['v1'] ?? '';
// Verify timestamp is recent (within 5 minutes)
if (abs(time() - (int) $timestamp) > 300) {
return false;
}
$signedPayload = "{$timestamp}.{$payload}";
$expectedSignature = hash_hmac('sha256', $signedPayload, $secret);
return hash_equals($expectedSignature, $signature);
}
Retry Policy
If your webhook endpoint returns a non-2xx status code, SOMA will retry with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5 | 1 hour |
| 6 | 4 hours |
After 6 failed attempts, the webhook is marked as failed.
Best Practices
- Respond quickly: Return a 2xx response within 30 seconds
- Handle duplicates: Use
event_idfor idempotency - Verify signatures: Always verify before processing
- Use HTTPS: Required in production
setup.completed
Sent when 100% of the payment has been collected from all participants.
This is the primary event you should listen for to fulfill orders.
Example request:
curl --request POST \
"http://soma.localhost/your-webhook-url" \
--header "Content-Type: application/json" \
--header "X-Soma-Signature: t=1705320000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd" \
--data "{
\"event_id\": \"01HQWBK123456789ABCDEFGH\",
\"event_type\": \"setup.completed\",
\"api_version\": \"2024-01-01\",
\"created_at\": \"2024-01-15T14:30:00+00:00\",
\"data\": {
\"setup_id\": \"01HQXYZ123456789ABCDEFGH\",
\"order_id\": \"order_12345\",
\"amount_total_cents\": 15000,
\"currency\": \"EUR\",
\"completed_at\": \"2024-01-15T14:30:00+00:00\",
\"metadata\": {
\"customer_email\": \"john@example.com\"
},
\"participants\": null
}
}"
const url = new URL(
"http://soma.localhost/your-webhook-url"
);
const headers = {
"Content-Type": "application/json",
"X-Soma-Signature": "t=1705320000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd",
"Accept": "application/json",
};
let body = {
"event_id": "01HQWBK123456789ABCDEFGH",
"event_type": "setup.completed",
"api_version": "2024-01-01",
"created_at": "2024-01-15T14:30:00+00:00",
"data": {
"setup_id": "01HQXYZ123456789ABCDEFGH",
"order_id": "order_12345",
"amount_total_cents": 15000,
"currency": "EUR",
"completed_at": "2024-01-15T14:30:00+00:00",
"metadata": {
"customer_email": "john@example.com"
},
"participants": null
}
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'http://soma.localhost/your-webhook-url';
$response = $client->post(
$url,
[
'headers' => [
'Content-Type' => 'application/json',
'X-Soma-Signature' => 't=1705320000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd',
],
'json' => [
'event_id' => '01HQWBK123456789ABCDEFGH',
'event_type' => 'setup.completed',
'api_version' => '2024-01-01',
'created_at' => '2024-01-15T14:30:00+00:00',
'data' => [
'setup_id' => '01HQXYZ123456789ABCDEFGH',
'order_id' => 'order_12345',
'amount_total_cents' => 15000,
'currency' => 'EUR',
'completed_at' => '2024-01-15T14:30:00+00:00',
'metadata' => [
'customer_email' => 'john@example.com',
],
'participants' => null,
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Your endpoint should return any 2xx status to acknowledge receipt.):
{
"received": true
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
payment.completed
Sent each time a participant successfully pays their share. Useful for tracking progress in real-time.
Example request:
curl --request POST \
"http://soma.localhost/your-webhook-url" \
--header "Content-Type: application/json" \
--header "X-Soma-Signature: t=1705320000,v1=abc123..." \
--data "{
\"event_id\": \"01HQWBK987654321ZYXWVUTS\",
\"event_type\": \"payment.completed\",
\"api_version\": \"2024-01-01\",
\"created_at\": \"2024-01-15T13:00:00+00:00\",
\"data\": {
\"participant\": {
\"id\": \"01HQPART123456789ABCDEFG\",
\"label\": \"Alice\",
\"amount_cents\": 7500,
\"paid_at\": \"2024-01-15T13:00:00+00:00\"
},
\"progress\": {
\"paid_amount_cents\": 7500,
\"remaining_amount_cents\": 7500,
\"progress_percentage\": 50
}
}
}"
const url = new URL(
"http://soma.localhost/your-webhook-url"
);
const headers = {
"Content-Type": "application/json",
"X-Soma-Signature": "t=1705320000,v1=abc123...",
"Accept": "application/json",
};
let body = {
"event_id": "01HQWBK987654321ZYXWVUTS",
"event_type": "payment.completed",
"api_version": "2024-01-01",
"created_at": "2024-01-15T13:00:00+00:00",
"data": {
"participant": {
"id": "01HQPART123456789ABCDEFG",
"label": "Alice",
"amount_cents": 7500,
"paid_at": "2024-01-15T13:00:00+00:00"
},
"progress": {
"paid_amount_cents": 7500,
"remaining_amount_cents": 7500,
"progress_percentage": 50
}
}
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'http://soma.localhost/your-webhook-url';
$response = $client->post(
$url,
[
'headers' => [
'Content-Type' => 'application/json',
'X-Soma-Signature' => 't=1705320000,v1=abc123...',
],
'json' => [
'event_id' => '01HQWBK987654321ZYXWVUTS',
'event_type' => 'payment.completed',
'api_version' => '2024-01-01',
'created_at' => '2024-01-15T13:00:00+00:00',
'data' => [
'participant' => [
'id' => '01HQPART123456789ABCDEFG',
'label' => 'Alice',
'amount_cents' => 7500,
'paid_at' => '2024-01-15T13:00:00+00:00',
],
'progress' => [
'paid_amount_cents' => 7500,
'remaining_amount_cents' => 7500,
'progress_percentage' => 50,
],
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Acknowledge receipt with any 2xx status.):
{
"received": true
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
setup.expired
Sent when a setup expires without being fully paid.
You may want to release any reserved inventory or notify the customer.
Example request:
curl --request POST \
"http://soma.localhost/your-webhook-url" \
--header "Content-Type: application/json" \
--header "X-Soma-Signature: t=1705320000,v1=abc123..." \
--data "{
\"event_id\": \"01HQWBKABCDEF123456789GH\",
\"event_type\": \"setup.expired\",
\"api_version\": \"2024-01-01\",
\"created_at\": \"2024-01-16T12:00:00+00:00\"
}"
const url = new URL(
"http://soma.localhost/your-webhook-url"
);
const headers = {
"Content-Type": "application/json",
"X-Soma-Signature": "t=1705320000,v1=abc123...",
"Accept": "application/json",
};
let body = {
"event_id": "01HQWBKABCDEF123456789GH",
"event_type": "setup.expired",
"api_version": "2024-01-01",
"created_at": "2024-01-16T12:00:00+00:00"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'http://soma.localhost/your-webhook-url';
$response = $client->post(
$url,
[
'headers' => [
'Content-Type' => 'application/json',
'X-Soma-Signature' => 't=1705320000,v1=abc123...',
],
'json' => [
'event_id' => '01HQWBKABCDEF123456789GH',
'event_type' => 'setup.expired',
'api_version' => '2024-01-01',
'created_at' => '2024-01-16T12:00:00+00:00',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Acknowledge receipt with any 2xx status.):
{
"received": true
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Errors & Status Codes
HTTP Status Codes
| Code | Description |
|---|---|
| 200 | Success |
| 201 | Resource created |
| 400 | Bad request - Invalid request format |
| 401 | Unauthorized - Missing or invalid API key |
| 403 | Forbidden - API key is valid but access denied |
| 404 | Not found - Resource doesn't exist |
| 409 | Conflict - Resource already exists (duplicate order) |
| 422 | Unprocessable entity - Validation error or business rule violation |
| 429 | Too many requests - Rate limit exceeded |
| 500 | Server error |
Error Response Format
All errors follow a consistent format:
{
"error": "error_code",
"message": "Human-readable description of the error",
"errors": {
"field_name": ["Validation error message"]
}
}
The errors field is only present for validation errors (422).
Error Codes Reference
Authentication Errors
| Code | HTTP Status | Description |
|---|---|---|
missing_api_key |
401 | No API key provided in the Authorization header |
invalid_api_key |
401 | API key format is invalid or key doesn't exist |
inactive_merchant |
403 | Merchant account is inactive |
Validation Errors
| Code | HTTP Status | Description |
|---|---|---|
validation_error |
422 | Request body failed validation |
invalid_currency |
422 | Currency not allowed for this merchant |
Business Logic Errors
| Code | HTTP Status | Description |
|---|---|---|
duplicate_order |
409 | An active setup already exists for this order_id |
not_found |
404 | The requested resource was not found |
invalid_state |
422 | Operation not allowed in current state |
has_payments |
422 | Cannot modify/cancel a setup that has received payments |
Error Codes
Reference documentation for all possible error codes returned by the API.
Use the error field to programmatically handle specific error conditions.
Example request:
curl --request GET \
--get "http://soma.localhost/errors/reference"const url = new URL(
"http://soma.localhost/errors/reference"
);
fetch(url, {
method: "GET",
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'http://soma.localhost/errors/reference';
$response = $client->get($url);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (401, Authentication error example):
{
"error": "invalid_api_key",
"message": "The provided API key is invalid."
}
Example response (422, Validation error example):
{
"error": "validation_error",
"message": "The given data was invalid.",
"errors": {
"amount_total_cents": [
"The amount must be an integer (in cents)."
],
"currency": [
"The currency must be a 3-letter ISO code (e.g., EUR, USD)."
]
}
}
Example response (409, Conflict error example):
{
"error": "duplicate_order",
"message": "An active setup already exists for order_id: order_12345",
"existing_setup_id": "01HQXYZ123456789ABCDEFGH",
"existing_setup_status": "collecting"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Setup Status Reference
A setup goes through the following lifecycle states.
Status Flow
draft → collecting → completed
↓
expired
↓
canceled
Status Descriptions
| Status | Description |
|---|---|
draft |
Setup created, waiting for participant configuration |
collecting |
Participants configured, collecting payments |
completed |
All payments received (100% collected) |
expired |
Setup expired before completion |
canceled |
Setup was canceled by the merchant |
Allowed State Transitions
| From | To | Trigger |
|---|---|---|
draft |
collecting |
Primary buyer configures participants |
draft |
canceled |
Merchant cancels via API |
draft |
expired |
Expiration time reached |
collecting |
completed |
Last participant pays (100% collected) |
collecting |
canceled |
Merchant cancels via API (only if no payments made) |
collecting |
expired |
Expiration time reached |
Participant Status
| Status | Description |
|---|---|
pending |
Waiting for payment |
paid |
Payment successfully received |
failed |
Payment failed |
canceled |
Participant was removed or setup was canceled |
Example request:
curl --request GET \
--get "http://soma.localhost/status/reference"const url = new URL(
"http://soma.localhost/status/reference"
);
fetch(url, {
method: "GET",
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'http://soma.localhost/status/reference';
$response = $client->get($url);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Example setup with various participant statuses):
{
"id": "01HQXYZ123456789ABCDEFGH",
"status": "collecting",
"participants": [
{
"id": "01HQ...",
"label": "Alice",
"status": "paid",
"amount_cents": 5000
},
{
"id": "01HQ...",
"label": "Bob",
"status": "pending",
"amount_cents": 5000
},
{
"id": "01HQ...",
"label": "Charlie",
"status": "failed",
"amount_cents": 5000
}
]
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Rate Limits
API requests are rate limited to ensure fair usage and system stability.
Rate Limits by Endpoint
| Endpoint | Limit |
|---|---|
POST /api/v1/somas |
100 requests per minute |
GET /api/v1/somas |
300 requests per minute |
GET /api/v1/somas/{id} |
300 requests per minute |
POST /api/v1/somas/{id}/cancel |
60 requests per minute |
Rate Limit Headers
Every response includes rate limit information:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1705320060
Handling Rate Limits
When rate limited, you'll receive a 429 Too Many Requests response:
{
"error": "rate_limit_exceeded",
"message": "Too many requests. Please retry after 60 seconds.",
"retry_after": 60
}
The Retry-After header indicates the number of seconds to wait before retrying.
Example request:
curl --request GET \
--get "http://soma.localhost/rate-limits/reference"const url = new URL(
"http://soma.localhost/rate-limits/reference"
);
fetch(url, {
method: "GET",
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'http://soma.localhost/rate-limits/reference';
$response = $client->get($url);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (429, Rate limit exceeded response):
{
"error": "rate_limit_exceeded",
"message": "Too many requests. Please retry after 60 seconds.",
"retry_after": 60
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.