MENU navbar-image

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

  1. Create a Setup - When a customer chooses "Pay with SOMA", create a setup via API
  2. Share the link - The primary buyer receives a link to configure the payment split
  3. Participants pay - Each participant pays their share via their unique payment link
  4. Order completed - When 100% is collected, SOMA notifies you via webhook

Key concepts

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
    }
}
 

Request      

GET api/v1/somas

Headers

Authorization        

Example: Bearer soma_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

status   string  optional    

Filter by setup status. Allowed: draft, collecting, completed, expired, canceled. Example: collecting

order_id   string  optional    

Filter by your external order ID. Example: order_12345

per_page   integer  optional    

Number of results per page (1-100). Default: 20. Example: 50

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."
        ]
    }
}
 

Request      

POST api/v1/somas

Headers

Authorization        

Example: Bearer soma_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Content-Type        

Example: application/json

Accept        

Example: application/json

Body Parameters

order_id   string     

Your unique identifier for this order. Used to prevent duplicates. Example: order_12345

amount_total_cents   integer     

The total amount to collect in cents. Example: 15000

currency   string     

ISO 4217 currency code (3 letters). Example: EUR

success_url   string     

URL to redirect participants after successful payment. Example: https://your-store.com/orders/12345/success

cancel_url   string     

URL to redirect participants if they cancel. Example: https://your-store.com/orders/12345/cancel

idempotency_key   string     

Unique key for safe request retries. Use a UUID or order-specific identifier. Example: order_12345_soma_v1

metadata   object  optional    

Optional custom data to attach to this setup. Will be included in webhooks.

expiration_minutes   integer  optional    

Optional minutes until the setup expires. Min: 5, Max: 10080 (7 days). Default: merchant setting or 1440 (24h). Example: 1440

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."
}
 

Request      

GET api/v1/somas/{id}

Headers

Authorization        

Example: Bearer soma_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The setup ID (ULID format). Example: 01HQXYZ123456789ABCDEFGH

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
}
 

Request      

POST api/v1/somas/{id}/cancel

Headers

Authorization        

Example: Bearer soma_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

id   string     

The setup ID (ULID format). Example: 01HQXYZ123456789ABCDEFGH

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:

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

  1. Respond quickly: Return a 2xx response within 30 seconds
  2. Handle duplicates: Use event_id for idempotency
  3. Verify signatures: Always verify before processing
  4. 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
}
 

Request      

POST your-webhook-url

Headers

Content-Type        

Example: application/json

X-Soma-Signature        

Example: t=1705320000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Body Parameters

event_id   string     

Unique identifier for this event (ULID). Use for idempotency. Example: 01HQWBK123456789ABCDEFGH

event_type   string     

The type of event. Example: setup.completed

api_version   string     

API version used for this webhook. Example: 2024-01-01

created_at   string     

When the event was created (ISO 8601). Example: 2024-01-15T14:30:00+00:00

data   object     

Event-specific data.

setup_id   string     

The setup ID. Example: 01HQXYZ123456789ABCDEFGH

order_id   string     

Your external order ID. Example: order_12345

amount_total_cents   integer     

Total amount in cents. Example: 15000

currency   string     

Currency code. Example: EUR

completed_at   string     

When the setup was completed. Example: 2024-01-15T14:30:00+00:00

metadata   object  optional    

Custom metadata you provided when creating the setup.

participants   object[]     

Array of participants and their payment details.

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
}
 

Request      

POST your-webhook-url

Headers

Content-Type        

Example: application/json

X-Soma-Signature        

Example: t=1705320000,v1=abc123...

Body Parameters

event_id   string     

Unique identifier for this event. Example: 01HQWBK987654321ZYXWVUTS

event_type   string     

The type of event. Example: payment.completed

api_version   string     

API version. Example: 2024-01-01

created_at   string     

When the event was created. Example: 2024-01-15T13:00:00+00:00

data   object  optional    
setup_id   string     

The setup ID. Example: 01HQXYZ123456789ABCDEFGH

order_id   string     

Your external order ID. Example: order_12345

participant   object     

The participant who just paid.

id   string     

Participant ID. Example: 01HQPART123456789ABCDEFG

label   string  optional    

Participant label/name. Example: Alice

amount_cents   integer     

Amount paid by this participant. Example: 7500

paid_at   string     

When the participant paid. Example: 2024-01-15T13:00:00+00:00

progress   object     

Current payment progress.

paid_amount_cents   integer     

Total amount paid so far. Example: 7500

remaining_amount_cents   integer     

Amount still to be collected. Example: 7500

progress_percentage   integer     

Completion percentage (0-100). Example: 50

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
}
 

Request      

POST your-webhook-url

Headers

Content-Type        

Example: application/json

X-Soma-Signature        

Example: t=1705320000,v1=abc123...

Body Parameters

event_id   string     

Unique identifier for this event. Example: 01HQWBKABCDEF123456789GH

event_type   string     

The type of event. Example: setup.expired

api_version   string     

API version. Example: 2024-01-01

created_at   string     

When the event was created. Example: 2024-01-16T12:00:00+00:00

data   object  optional    
setup_id   string     

The setup ID. Example: 01HQXYZ123456789ABCDEFGH

order_id   string     

Your external order ID. Example: order_12345

amount_total_cents   integer     

Total amount that was to be collected. Example: 15000

currency   string     

Currency code. Example: EUR

paid_amount_cents   integer     

Amount that was paid before expiration (may need refund). Example: 7500

expired_at   string     

When the setup expired. Example: 2024-01-16T12:00:00+00:00

metadata   object  optional    

Custom metadata.

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"
}
 

Request      

GET errors/reference

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
        }
    ]
}
 

Request      

GET status/reference

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
}
 

Request      

GET rate-limits/reference