# Get started

Learn about authentication, permissions, error handling, and API key management.

## OpenAPI specification

The complete API schema is available as a single bundled file. It includes all endpoints, request and response structures, and field definitions. To integrate programmatically, download the specification in the preferred format, instead of parsing the rendered API reference:

YAML
    
JSON
    
## Authentication and security

The Solidgate API v2 uses a simplified **Bearer token** authentication model for outbound API calls. You no longer need to manage request signatures or multiple key sets when calling the API. Incoming webhook deliveries still require [HMAC signature verification](#webhook-validation).

**API key**

- **Format:** Key IDs use the prefix `akey_xxx`, secrets use the prefix `asec_xxx`.
- **Method:** Include the secret in the `Authorization` header of every request.


Example header

```text
Authorization: Bearer asec_your_secret_here
```

**Key flexibility**

API keys are decoupled from channels.

- **Account level:** By default, keys exist at the account level with access to all channels.
- **Channel-bound:** A key that can be restricted to one or many specific channels.


## API structure and conventions

The API follows a strict structural pattern to keep behavior predictable.

**Endpoint format**

All operations use the **POST** method.

```text
POST {host}/{version}/{resources}/{action}
```

- **Host:** `https://api.solidgate.com`
- **Version:** `v2`
- **Resources:** Plural form of the domain model, for example, `api-keys`, `payments`.
- **Action:** Operation name, for example, `create`, `list`, or `rotate`.


**Data formatting**

- **Body:** JSON
- **Property names:** `snake_case`
- **Enum values:** `UPPER_CASE`


**Reading data: get, list, and search**

Resources offer up to three read actions. Pick the one that matches the job.

- **`get`:** Returns one resource by its identifier. Always reflects the current state, so use it before acting on a resource, for example, before cancelling or updating it.
- **`list`:** Returns a paginated page of resources using a small fixed set of filters, for example, by customer or creation date. Use it to load all resources of a known owner or for a date range. Results can briefly lag behind recent changes.
- **`search`:** Returns a paginated page of resources matching flexible filters, for example, by status, email, or product, combined in one request. Results are eventually consistent and lag more than `list`: new and updated resources are not immediately searchable, and returned fields can lag behind the latest state.


Only `get` reflects the latest state, so do not use `list` or `search` for read-after-write flows, for example, looking up a subscription right after creating it. Use `list` or `search` to find resources, then call `get` when you need the up-to-date state of a specific one.

## Scopes and permissions

Fine-grained access control follows the principle of least privilege. Keys can be restricted by **channel** and **permission**.

## API key management

Manage keys via **Solidgate Hub** or **API v2**. Hub access is available to Merchant Admin and Developer roles. Navigate to the **Developers** section and then select **API v2**.

**Key rotation with zero downtime**

To maintain security without service interruption, use **Rotation**.

Initiate
API:
Call 
/rotate
and set the rotation period in seconds
Hub:
Click 
Rotate
for the API key
Overlap
During this period, both old and new secrets remain valid
      
Expiry
After the period ends, the old secret is automatically deactivated
      
**API key operations**

All operations require an account-level API key marked in Hub as **Applies to all channels**.

Endpoint
Operation
POST /v2/api-keys/create
Create API key
POST /v2/api-keys/list
List API keys
POST /v2/api-keys/get
Get API key details
POST /v2/api-keys/rotate
Rotate API key
## Error handling

The API uses standard HTTP status codes. All errors return the same JSON envelope, for example:

```json
{"code": "PERMISSION_DENIED", "message": "Permission denied"}
```

Some responses include a `context` object with structured details. Every response carries a `request-id` header, share it with support when reporting issues.

Status
Code
Description
400
VALIDATION
Malformed JSON or invalid field constraints. 
context.constraints
lists per-field failures.
401
UNAUTHENTICATED
Invalid or missing API key.
403
PERMISSION_DENIED
Key lacks the required scope, or channel-bound key calling outside its channels.
404
NOT_FOUND
Resource does not exist.
404
ENDPOINT_NOT_FOUND
Endpoint does not exist.
409
IDEMPOTENCY_KEY_IN_USE
A request with the same idempotency key is still in progress. Retry after the first request completes.
422
varies
Request conflicts with current business or system state, for example 
ENDPOINT_ALREADY_EXISTS
or 
IDEMPOTENCY_KEY_REUSED
.
429
RATE_LIMIT
Quota exhausted. Check 
context.next_try_at
.
500
INTERNAL
Server-side failure.
## Rate limits

Rate limiting controls the frequency at which requests are made to API endpoints within specific time periods.

It helps protect against service overload while ensuring consistent performance for all clients. Exceeding limits results in a `429 Too many requests` error response.

**API usage limits**

Solidgate returns the `429` error response when necessary to protect legitimate merchant traffic.

Rate limits differ by endpoint based on operational and reliability needs. The Solidgate team continuously monitors system performance and may adjust these limits as needed to maintain optimal service quality.

For endpoint-specific rate limit information, visit the **Developers** section in the Solidgate Hub, which is updated as changes occur.

**Handle rate limits**

You can handle rate limiting by monitoring for the `429 Too many requests` error response. Effective handling combines retries and overall request flow control.

A widely used approach for handling rate limit error responses is implementing exponential backoff with jitter. This method retries requests using short initial delays that increase after each failure. Introducing randomization, or jitter, helps avoid conflicts caused by multiple clients retrying simultaneously.

While retries are useful, a significant improvement comes from regulating request flow across the entire application. The token bucket is standard practice for this purpose. It allows short bursts of requests while enforcing an average request rate over time, reducing traffic spikes and improving overall stability.

## Idempotent requests

Idempotency reduces duplicate operations when you retry a mutating request, for example, after a timeout or a network error. It does not guarantee that an operation runs exactly once. This is separate from deduplicating [webhook deliveries](#webhook-validation), which use the `solidgate-event-id` header instead.

To make a request idempotent, send an `Idempotency-Key` header with a unique value for each distinct operation. The key is scoped to your account, endpoint, and environment, so the same key sent to a different endpoint or environment does not count as a reuse.

**Generate idempotency keys**

Generate a new key for each new operation. Do not reuse a key across different operations, and do not base a key on a business identifier, such as an order ID or invoice ID. One draft invoice can have multiple updates, such as setting the customer’s email and then correcting it. If both requests use the invoice ID as the key, the second request has a different body but reuses the same key. The API returns a `422` error and does not apply the update. A version 4 universally unique identifier (UUID) is a good choice.

An idempotency key is a string of 1 to 255 printable ASCII characters. Spaces and control characters are not valid and return a `400` error.

Idempotency support varies by endpoint. Send the request with an `Idempotency-Key` header and check the `Idempotency-Status` value. A value of `ignored` means the endpoint does not support idempotency.

If a request fails due to a network error or a timeout and the outcome is unclear, retry it with the same idempotency key. The original request might have already completed, so the retry returns the saved result instead of running the operation again.

**Response behavior**

Each response to a request sent with an `Idempotency-Key` header includes an `Idempotency-Status` header. The value describes how the key was handled.

Value
Description
stored
The request ran, and the result is saved under this key for future replay.
replayed
The response is a replay of a previous request with the same key.
not-stored; reason=unavailable
The idempotency feature was unavailable, so the API ran the request without idempotency protection.
ignored
The endpoint does not support idempotency. The key has no effect on the request.
`not-stored` includes a `reason` parameter, for example `not-stored; reason=unavailable`.

Stored and replayed responses also include `Idempotency-Created-At` and `Idempotency-Expires-At` headers. These report the timestamp of the first request and the time the saved result expires.

A saved result expires 24 hours after the first request. After this period, the API treats the same key as new.

A replayed response includes the original status code, body, `Content-Type` header, and `Location` header. Other headers might differ from the original response.

If a request fails validation or authentication before the API registers the idempotency key, the response does not include an `Idempotency-Status` header. Retry the request after you resolve the issue.

**Conflicts and errors**

A request returns `409 IDEMPOTENCY_KEY_IN_USE` when a request with the same key is still in progress. It returns `422 IDEMPOTENCY_KEY_REUSED` when the same key is reused on the same endpoint with a different request body. Both codes are listed in [error handling](#error-handling).

The API compares request bodies as parsed JSON, so differences in field order or whitespace do not trigger this error.

A `409` response includes a `Retry-After` header indicating how long to wait before you retry with the same key.

A client-side timeout or network error does not use the key. Retry the request with the same key. If the original request is still in progress, the retry returns `409 IDEMPOTENCY_KEY_IN_USE`. If the original request already completed, the retry returns the saved result.

The API saves and replays a `5xx` response like any other response, for up to 24 hours. Check the resource state before you retry, then retry with a new key only if the operation still needs to run.

Authentication errors (`401`/`403`) and request validation errors (`400`) happen before the API registers the key. Fix the request, then retry with the same key.

Other error responses register the key and are replayed on retry. Use a new key to run the operation again with different input.

**Idempotency is best effort**

Idempotency reduces duplicate operations caused by retries, but it does not guarantee that an operation runs exactly once. In rare cases, retrying a request with the same idempotency key can run the operation again:

- The idempotency feature is temporarily unavailable. The API does not record the key. The response includes `Idempotency-Status: not-stored; reason=unavailable`.
- The original request fails or takes unusually long before its result is saved. The operation might have taken effect before the failure or delay interrupted processing.


To reduce the risk of duplicate operations:

- If you receive a `409` response with code `IDEMPOTENCY_KEY_IN_USE`, wait for the duration specified in the `Retry-After` header, then retry with the same key. The original request might still be processing.
- If you receive no response, the outcome is unknown. Check whether the operation took effect before you retry. If you retry, use the same key.
- If the response includes `Idempotency-Status: not-stored`, idempotency protection did not apply to the request. Check whether the operation took effect before you submit it again.
- If the response includes `Idempotency-Status: stored` or `replayed`, the retry was safe and no further action is needed.
- When duplicates would be costly, also set a business reference such as the `merchant_reference` [field](https://api-docs.solidgate.com/api/v2/subscriptions/create-subscription#subscriptions/create-subscription/t=request&path=merchant_reference), if the endpoint supports one, and look up records by it before you create a new one.


**Idempotency and business references**

Idempotency deduplicates based on the `Idempotency-Key` header alone, and the key is valid for 24 hours. Invoices and subscriptions also support a business reference field, such as `merchant_reference`, for longer-term deduplication. Use this field to look up or deduplicate records by your own identifiers, independent of the idempotency key.

▸
Idempotency example
This example illustrates how idempotency works when creating a subscription.

**1. Create a subscription with a key**

```http
POST /v2/subscriptions/create
Idempotency-Key: 9f8b7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d

{
  "customer_id": "cust_01KMG2ABYPF6XS5DMJQ817C429",
  "payment_instrument_id": "pi_01J1Z2X3Y4W5V6U7T8S9R0",
  "line_items": [{ "product_price_id": "e6f26db1-f633-4723-ab08-50ddee7bc648", "quantity": 1 }]
}
```

**2. Request times out. Retry with the same key and request body**

```http
HTTP/1.1 200 OK
Idempotency-Status: replayed
Idempotency-Created-At: 2026-10-24T11:45:00Z
Idempotency-Expires-At: 2026-10-25T11:45:00Z

{ "id": "subs_01HYDYBRKN16B8X3BR9WP78ZR7", "status": "ACTIVE" }
```

**3. Reuse the same key with a different request body**

```http
HTTP/1.1 422 Unprocessable Entity

{ "code": "IDEMPOTENCY_KEY_REUSED", "message": "idempotency key already used with a different request" }
```

**4. Retry while the first request is still in progress**

```http
HTTP/1.1 409 Conflict
Retry-After: 1

{ "code": "IDEMPOTENCY_KEY_IN_USE", "message": "a request with this idempotency key is still in process" }
```

**5. Send a request with an invalid idempotency key**

```http
HTTP/1.1 400 Bad Request

{
  "code": "VALIDATION",
  "message": "request validation failed",
  "context": {
    "constraints": {
      "Idempotency-Key": { "type": "FORMAT", "message": "must be 1 to 255 printable ASCII characters" }
    }
  }
}
```

**6. Request is rejected before processing (`400` body validation, `401`, `403`)**

The response has no `Idempotency-Status` header, and the API does not save the key. Fix the request, then send it again with the same key.

**7. Processing finishes with an error (`4xx` business errors such as `422 CUSTOMER_NOT_FOUND`, and `5xx`)**

The error is saved like a success: `Idempotency-Status: stored`, and retries with the same key return `replayed` for 24 hours. To try again, check the resource state, then send the request with a new key.

**8. Response is not saved**

```http
HTTP/1.1 200 OK
Idempotency-Status: not-stored; reason=unavailable
```

**9. Key expires (after `Idempotency-Expires-At`)**

After the time specified by `Idempotency-Expires-At`, the key can be used again. A request with that key is treated as a new request.

## Webhook validation

Webhook event security uses a Base64-encoded HMAC-SHA256 signature generated with your webhook endpoint secret. Each notification includes a `signature` value in the headers.

- `signature` – a Base64-encoded HMAC-SHA256 digest of the raw request body, signed with your webhook endpoint secret.


Unlike v1, webhook deliveries do not include a public key in the headers. Identify the correct webhook endpoint secret using the endpoint that received the webhook.

### Verify a signature

1. Determine which endpoint received the webhook, and look up that endpoint's secret.
2. Generate a signature from the raw request body using the `generateSignature` function, which must return the Base64-encoded digest.
3. Compare your generated signature to the `signature` header value using a constant-time comparison function, for example `hash_equals` in PHP, `hmac.compare_digest` in Python, `crypto.timingSafeEqual` in Node.js, `hmac.Equal` in Go, `MessageDigest.isEqual` in Java or Kotlin, or `CryptographicOperations.FixedTimeEquals` in C#. Do not use `==` or `.equals()`.
4. If verification fails, retry with the previous secret before rejecting the request, in case you are inside a rotation overlap window. The old and new secrets both stay valid during rotation.
5. Reject the request without processing it if verification fails against both secrets.


Use the raw JSON body exactly as received, with no changes. Serializers, URL encoding, or reformatting can change the byte structure and produce a different hash, causing a valid webhook to fail verification.

.sg-prompt-accordion > div > :first-child { margin-top: 0; }
    .sg-prompt-accordion .sg-prompt-chevron { display:inline-block; transition:transform .15s; }
    .sg-prompt-accordion[open] .sg-prompt-chevron { transform: rotate(90deg); }
    
▸
AI prompt to verify v2 webhook signatures
Copy
Implement Solidgate v2 webhook signature verification for my integration.

Context: Solidgate v2 webhooks carry a `signature` verification header (a base64-encoded HMAC-SHA256 of the raw body, signed with a per-endpoint secret prefixed `wsec_`), along with `solidgate-event-id`, `solidgate-event-type`, and `solidgate-event-occurred-at` headers. The `event_id`, `event_type`, `occurred_at`, and `data` values also appear in the JSON body. Each webhook endpoint I registered has its own secret. There is no public key or endpoint identifier in the request itself, so the receiving URL tells me which stored secret to use.

Algorithm to implement:

1. Look up the secret (`wsec_...`) I stored for the specific endpoint URL that received this request.
2. Read the raw request body as bytes, before any JSON parsing or reserialization. The exact byte sequence affects the hash.
3. Compute HMAC-SHA256 of the raw body using that secret.
4. Base64-encode the raw digest bytes directly. There is no hex step. This differs from Solidgate v1, which hex-encodes first, so do not reuse v1 code here.
5. Compare the result to the `signature` header using a constant-time comparison. Use `hash_equals` in PHP, `hmac.compare_digest` in Python, `crypto.timingSafeEqual` in Node.js, `hmac.Equal` in Go, `MessageDigest.isEqual` in Java or Kotlin, or `CryptographicOperations.FixedTimeEquals` in C#. Do not use `==` or `.equals()`.
6. If verification fails, retry with my previous secret before rejecting, in case I am inside a rotation overlap window. Old and new secrets both stay valid during rotation.
7. Reject the request without processing it if verification fails against both secrets.


Also implement:

- Idempotency: deduplicate using the `solidgate-event-id` header. Store processed event IDs for at least one week (7 days) and skip duplicates seen within that window.
- Delivery order: do not assume ordering. Delivery order is not guaranteed, so the same event can arrive more than once and out of sequence.
- Ordering (if needed): if my integration needs to apply events in the correct order, sequence them using the `solidgate-event-occurred-at` header on a best-effort basis. This field reflects when the event occurred, not when it was delivered, so treat it as a hint rather than a guarantee.
- Fast response: verify the signature, durably enqueue the payload, then return a 200 status immediately. Process the queued payload asynchronously in a background worker rather than running business logic inside the request handler.


After generating the code, list each requirement above (secret lookup, signature algorithm, rotation fallback, idempotency, ordering, fast response) and state how the implementation satisfies it. If any requirement is not addressed, say so explicitly instead of omitting it.

Write this in my language and framework. Ask me which one if it is not clear from my codebase, and match my project's existing conventions for reading raw request bodies and structuring webhook handlers.

### Delivery headers

Every v2 webhook delivery includes the following headers.

Header
Type
Description
Example
signature
string
Base64-encoded HMAC-SHA256 of the raw request body, signed with your webhook endpoint secret.
MjNiYW...
solidgate-event-id
string
Unique ID for the event. Use it to deduplicate deliveries.
3f1c8a52-0b6d-4d3e-9c2a-1e8b7f4a90d1
solidgate-event-occurred-at
string
(date-time)
Date and time when the event occurred. Use it to order events chronologically.
2026-04-03T16:18:40.000000Z
solidgate-event-type
string
The type of event that occurred.
SUBSCRIPTION_CREATED
### Delivery payload

Every v2 webhook payload includes the following fields.

Field
Type
Description
Example
event_id
string
Unique ID for the event. Use it to deduplicate deliveries.
3f1c8a52-0b6d-4d3e-9c2a-1e8b7f4a90d1
event_type
string
The type of event that occurred.
SUBSCRIPTION_CREATED
occurred_at
string
(date-time)
Date and time when the event occurred. Use it to order events chronologically.
2026-04-03T16:18:40.000000Z
data
object
The event payload. Structure depends on 
event_type
.
—
The same `event_id`, `event_type`, and `occurred_at` values are also sent as the `solidgate-event-id`, `solidgate-event-type`, and `solidgate-event-occurred-at` headers.

### Best practices

- **Reduce replay risk**: Check the `occurred_at` field in the payload against the current time, and reject deliveries with an implausibly old timestamp. Choose a cutoff wide enough to tolerate retries and delivery delays. This check reduces risk but does not fully prevent replay, since a valid signed payload can still be replayed within the accepted window. Use `solidgate-event-id` deduplication as the primary control against duplicate processing.
- **Process asynchronously**: Return a `200 OK` response immediately after signature verification, then handle the payload in a background queue. This avoids timeout failures during traffic spikes.
- **Ensure idempotency**: Delivery order is not guaranteed, and the same event may arrive more than once with the same `event_id`. Store processed event IDs for at least one week, and skip any duplicate. This deduplication is separate from the [`Idempotency-Key` header](#idempotent-requests) used for request retries.


## Backward compatibility

Breaking changes can impact existing integrations and require adjustments. These are marked with a **breaking changes** badge in the changelog and include:

Category
Change
Operation removal
Removing an API operation.
Request
Remove or rename a field, make optional fields required, remove 
oneOf
.
Response
Remove or rename a field, change HTTP status code, remove 
oneOf
.
Type changes
Change request or response data types.
HTTP headers
Add required headers or remove existing ones.
Enum updates
Remove enum values.
Errors
Change existing error codes.
Validation rules
Add stricter or new rules.
Authentication and authorization
Change requirements.
Non-breaking changes do not affect existing integrations and preserve backward compatibility:

Category
Change
Request
Add new optional fields, change required fields to optional.
Response
Add new optional fields, change optional fields to required.
HTTP headers
Add new optional headers, change header case.
Field length
Expand maximum length.
Identifier format
Change prefixes or formatting.
Webhook events
Add new opt-in event types.
Webhook schema
Add new fields.
Rate limiting
Changes communicated at least one month in advance.
New enum values can appear in any field whose description says `The list of enum values may be extended without notice.` These values can be added without a new API version. Handle unknown values in a default branch instead of treating them as errors. In responses and webhooks, enums without this note get new values only in a new API version. A field without this note can become extensible only by gaining this note. An extensible field may later be documented as a fixed set.

For help, contact us.