# Getting started with the API

Find the API reference, create an API key, authenticate your first call, and handle idempotency, rate limits, and timestamps correctly.

**Category:** Integration

**Last reviewed:** 6 October 2026

# Getting started with the API

Everything you can do in the Syntch portal, you can do over the API: take a payment, save a card, raise an invoice, look up a customer, pull a report. This guide gets you from a portal login to an authenticated call, shows you where the reference lives, and covers the three things every integration needs to get right afterward: idempotency, rate limits, and timestamps.

You only need two things to start: an account in the portal, and a few minutes to create an API key.

## Where the reference lives

The reference is inside the product, at **/api-docs**. Sign in and open it, and you get the live specification for the deployment you are signed in to, so the paths and schemas you read are the ones your calls will hit. It lists every operation the deployment publishes, which is what makes it useful for planning an integration: you can see an operation, and what it requires, before you hold the permission to run it.

Four things make it worth using rather than working from a copied specification file:

- **Each operation states the permission it needs.** Select an operation and its required permissions are listed alongside the request and response detail, so you can see what a key's owner has to hold before you write the call. Permissions are enforced when the call runs, so the reference and the runtime agree.
- **Schemas expand on demand.** Select an operation to see its request and response shapes, then drill into any nested type from the schema explorer.
- **Try it runs a real call.** The Try-it pane builds a request, prefills the first example payload for the operation, and posts it. The auth toggle defaults to **API Key**, which is how an integration authenticates, so what you exercise there matches what your code will do. The console below it keeps a log of requests and responses across operation switches, so you can compare two calls side by side.
- **Code samples come with the operation.** Each one renders as cURL, Python, and .NET, with the API key header as the primary auth mechanism.

The page toolbar also offers the OpenAPI document itself as JSON or YAML, if you want to generate a client from it.

### Pull the specification file

Two places publish the specification, and they answer different questions.

**Your deployment's live contract.** Pull it from the deployment you integrate against:

```
GET https://your-deployment.example.com/openapi/v1.json
GET https://your-deployment.example.com/openapi/v1.yaml
```

Both are anonymous, so a code generator can fetch them without a credential. The older `/swagger/v1/swagger.json` path still works and redirects to the JSON address, so update any bookmark or build script that still uses it. A successful response carries an `ETag` and `Cache-Control: no-cache`, so send `If-None-Match` on later pulls and a `304 Not Modified` tells you the contract hasn't changed. A `503` carries neither, so don't treat it as a cacheable answer. If an instance answers `503` with a `Retry-After` header, it hasn't produced a document yet. Usually it's still starting up, but a `503` that persists past a few retries means generation is failing on that instance, so report it rather than continuing to poll. You never receive a partial document.

Don't point a generator at `/api-docs`. That's the reference page for people, it requires a sign-in, and a generator asking it for a specification gets sign-in HTML back.

**Versioned SDK releases.** Whenever the contract changes, the generated client libraries are released under a new semantic version, together with a Postman collection, a changelog and a checksum list. Each release states the contract revision it was generated against, which you can compare with `GET /api/platform/contract` on the instance you call. Pin a release version if you want a stable, reviewable baseline rather than whatever a deployment serves today.

### What the specification does and doesn't depend on

**Every caller gets the same document.** It isn't rendered for your API key, your permissions, your merchant, or your account. Two callers pulling the same instance at the same moment receive identical bytes. What you can successfully *call* still depends on your permissions, but the document doesn't shrink to match them.

**It reflects the deployment's feature settings at the moment it was generated.** Operations behind an optional feature, such as digital wallets or surcharging, are present only when that feature is turned on. Once an instance is warmed up it regenerates on its own schedule rather than on your request, so two pulls taken minutes apart while a feature is being turned on or off can legitimately differ, and during a rolling update two instances behind one address can serve different documents for a short period. This is the usual explanation for a diff you didn't expect between two same-day pulls. It isn't caching, and it isn't the document being tailored to you.

If two pulls disagree, pull again once the change has settled and compare the `ETag` values. Matching ETags mean matching documents.

### Which document to generate a client from

- **Prefer a published SDK.** The catalog in the portal lists the generated client libraries, each release already tested against a sandbox.
- **Generating your own client for a specific deployment?** Use that deployment's `/openapi/v1.json`. It's the only source that includes the optional features that deployment has turned on.
- **Want a fixed target to pin and review?** Pin an SDK release version. Each release is generated with every optional feature at its default setting, so operations behind digital wallets or surcharging aren't in it even if your deployment has them enabled. Take the live document instead when you need those.

One more document can be mistaken for this one. Deployments that still host the older compatibility API expose a separate specification, also labeled v1, describing that older surface. It isn't the contract described here, and it isn't the one to build a new integration against.

### Start from the screen you already know

You don't have to hunt through the tree to find the operations behind a workflow. Portal pages carry a **`</>`** button in the toolbar that lists the API operations that page uses, split into the ones it owns and the ones it touches incidentally. Each entry deep-links into `/api-docs` with the operation already selected. The button appears on a page whose operations you hold the permissions to call, so what you see through it matches what you can invoke.

That's usually the fastest route into the reference: do the thing once in the portal, press `</>` on that screen, and read the operations that did it.

## Authenticate with an API key

An API key is the primary credential for a server-side integration. It's a long random string you send on every request, and it needs no interactive login.

### Create a key

Go to **/ApiKeys** and create one. You give it two things:

- A **name**, so you can tell your integrations apart later.
- An optional **expiration date**, from tomorrow up to a year out. The key stops working at the start of that day in your timezone, and the picker states which timezone that is.

**Copy the key when it's shown.** The full value is revealed once, at creation, with a copy button next to it. After that the portal shows only a masked form, and the key's detail page carries no field that could hold the secret. If a key is lost, delete it and create a new one.

Store the key the way you store any other production secret: in your secret manager or environment configuration, never in source control and never in browser-side code.

### Send the key

Put the key in the `api-key` request header on every call:

```
POST /api/transactions
api-key: YOUR_API_KEY
Content-Type: application/json
```

As cURL:

```bash
curl -X POST https://your-gateway-host/api/transactions \
  -H "api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "...": "..." }'
```

That's the whole handshake: no separate login step, and no token to refresh.

### What a key can do

A key belongs to the user who created it and carries that user's identity: the same permissions, the same tenant, and the same merchant or reseller scope. A request made with the key flows through exactly the same authorization checks an interactive session does, which means a key can never reach further than the person holding it.

That makes scoping an integration a matter of choosing the right owner. Give each integration its own user, with a role that grants only what that integration needs, then sign in as that user to create its key. A reporting job and a payment service can then hold genuinely different reach, and revoking one is a matter of deleting one key. See [Creating and managing users](/help/guides/creating-and-managing-users) for building the role and choosing the scope.

### Narrow a key with scopes

A key can also be restricted to a set of **capability scopes**, which is a second, narrower boundary drawn on top of the owner's permissions.

**Scopes only restrict. They never grant.** A key still authenticates as its owner and still runs through exactly the same authorization checks, so what the key can actually do is the overlap between the two: the owner's permissions, narrowed by the key's scopes. Selecting a scope for something the owning account can't do changes nothing, and no combination of scopes can reach past what that account holds.

That's why scopes are worth adding even where the owner is already a purpose-built user. The owner decides the ceiling; the scopes decide how much of that ceiling one particular credential can use. A key you paste into a batch job can be limited to reading transactions even though its owner could also take payments, so a leaked copy of that key can't.

The vocabulary is fixed:

| Scope | What it reaches |
|-------|-----------------|
| `transactions:read` | Look up transactions, transaction reports and batch files; read hosted payment page configuration and sessions, campaigns, promotion codes, shipping origins and parcel presets; request shipping rate quotes. |
| `transactions:write` | Create and update transactions, including fraud review and partial approval decisions; upload and manage batch files; create and manage hosted payment pages, hosted payment page sessions, campaigns, promotion codes, shipping origins and parcel presets; run sandbox settlement and recurring billing on demand. Includes everything `transactions:read` reaches. |
| `transactions:refund` | Run follow-up operations against an existing transaction, including refunds, voids and captures. Includes everything `transactions:read` reaches. |
| `customers` | Read and manage customer records and their stored payment methods. |
| `tokens` | Create, resolve and manage payment tokens, including wallet tokenization. |
| `webhooks` | Manage webhook destinations, channels and event subscriptions. |
| `reports` | Read usage, settlement and recurring billing reports. |
| `admin` | The back office: merchants, resellers, users, roles, invoicing, announcements, rate limits and key management. Most integrations don't need this. |

Know four things before you use them:

- **No scopes means no restriction.** That's the default, and it's what every key created before scopes existed carries. Those keys keep working exactly as they did. Clearing every scope on a key returns it to that state.
- **A refund isn't a write.** The operation on a follow-up call is named in the request body, so the platform can't tell a refund from a capture by the route alone. The whole follow-up family therefore sits behind `transactions:refund`, and a key holding only `transactions:write` is refused it.
- **The picker dims what the owner can't use.** On both the portal and the back office, a scope the key's owning account holds no permission for is shown but can't be selected, with a note saying why. In the back office that's judged against the key's owner, not against you, because the key will authenticate as its owner.
- **Rotation carries scopes across.** A rotated key is a replacement with the same access, so it inherits both the scopes and the source-address allowlist of the key it replaces.

Scopes are stored on the key and can be changed later from **/ApiKeys** without reissuing it. A change takes effect on the next request.

### Watch a key in use

Open a key from the list at **/ApiKeys** to see what it has been doing:

- **Last Used**, the most recent moment the key authenticated anything at all, including calls that create no transaction.
- **Recent Transactions**, the newest transactions submitted with the key, each linking to its detail page.
- **Source IPs**, the addresses the key has recently authenticated from, newest first, with first-seen and last-seen times and a link out to geolocation.

Last Used and Source IPs are recorded on the authentication path and written in batches, so they can trail live traffic by a few minutes. A source address never seen before is recorded without that delay, which is the signal to watch: if a key starts authenticating from somewhere you don't recognize, delete it and issue a new one.

### Expiration

A key with an expiration date is warned about before it lapses, so the first sign is never a failed call. By default the owner is notified 30 days out, again at 7 days, and again on the last day. Each warning arrives as a notification in the portal for the key's owner, and the `ApiKey.Expiring` event can also be routed to email or any other destination through a notification subscription. [API key expiration reminders](/help/guides/api-key-expiration-reminders) covers the full story: the reminder cadence, how to route the event, its filter fields, and what a webhook receiver gets.

You don't have to wait for a warning to find out. A key inside the same window shows an **Expiring soon** badge everywhere keys are listed, both in the API Keys grid and in the Developer Portal, so the countdown is visible the moment you look rather than only when a reminder arrives. The badge uses the same window the reminders use, so if your administrator moves the threshold, the badge moves with it. A key with no expiration date never shows the badge, and neither does anything already revoked, expired, or mid-rotation: those states are shown instead, because each one tells you something more urgent.

Plan the rollover the same way you would a certificate: create the replacement key, deploy it, confirm traffic has moved by watching Last Used on both keys, then delete the old one.

### Common authentication errors

A refused key comes back as **HTTP 401** with a JSON body carrying a `code`. The code is the part to branch on: the six values are stable, and each one points at a different fix.

```json
{
  "error": "Unauthorized",
  "code": "KEY_EXPIRED",
  "message": "API key has expired."
}
```

| Code | What happened | What to do |
|------|---------------|------------|
| `KEY_INVALID` | The value you sent doesn't match any key, or isn't a key at all. A mistyped, truncated, or already-deleted key all land here. | Check that the header is `api-key` and that the value is the full string you copied at creation, with no whitespace and nothing trimmed. If the key was deleted, create a new one. |
| `KEY_REVOKED` | The key exists but has been taken out of service, and it will never be accepted again. | Create a replacement key and deploy it. Extending anything on the old key won't bring it back. |
| `KEY_SUSPENDED` | The key has been taken out of service temporarily, either by an administrator or automatically after the platform saw it used from a network it had never been used from before. It hasn't been revoked. | Ask an administrator to reinstate the key from its detail page at **/ApiKeys**. Reinstating lifts the suspension from the same key value. If anything else is wrong with the key, the next response names that code instead. Don't create a replacement: a new key leaves the suspended one to be reinstated behind you. |
| `KEY_EXPIRED` | The key exists and was in service, but it has passed the expiration date it was created with. | Create a replacement key. The owner is warned 30 days, 7 days, and 1 day ahead, so wire those notifications somewhere your team reads. |
| `KEY_ENVIRONMENT_MISMATCH` | The key is in service, but it was minted for the other environment than the one its merchant is in today. An `sk_test_` key against a merchant that has since gone live, or an `sk_live_` key against a merchant that's no longer trading live. | Create a new key for the merchant now. A key's environment is fixed when it's created and is never re-stamped, so a key that spans a merchant's go-live has to be replaced. |
| `TRIAL_EXPIRED` | The key is valid and in service, but the merchant it belongs to is on a time-limited plan whose deadline has passed. Nothing is wrong with the credential. | Talk to your integration contact about extending the trial. Creating a new key won't help: no key for that merchant is accepted until the trial is extended, and the existing keys work again as soon as it is. |

Three things worth building into your client:

- **Don't retry any of them.** None is a transient condition, so retrying the same key produces the same answer. Surface the code and stop; a retry loop against a revoked key just fills your logs.
- **Route each code to the fix it names.** `KEY_INVALID`, `KEY_REVOKED`, `KEY_EXPIRED`, and `KEY_ENVIRONMENT_MISMATCH` are terminal for the key you sent: correct the value or create a new key. `KEY_SUSPENDED` isn't terminal: an administrator can reinstate the same key, so send it to someone who can do that rather than to your key-rotation path. `TRIAL_EXPIRED` is about the merchant's plan rather than the key, so send it to whoever manages that plan; a new key won't clear it.
- **Log the code, never the key.** The code is the diagnostic; the value you sent is a live credential and belongs nowhere but your secret store.

The response never says which environment the merchant is in, and never confirms whether a value it rejected corresponds to a real key belonging to somebody else. If you need to know why a specific key stopped working, its detail page at **/ApiKeys** carries the state and the expiry.

### When the key is fine but the scope isn't

One more code isn't an authentication failure at all. A key that authenticates but isn't scoped for the operation it addressed comes back as **HTTP 403**:

```json
{
  "error": "Forbidden",
  "code": "KEY_SCOPE_INSUFFICIENT",
  "message": "This API key is not scoped for this operation. It requires the 'transactions:write' scope.",
  "requiredScope": "transactions:write"
}
```

| Code | What happened | What to do |
|------|---------------|------------|
| `KEY_SCOPE_INSUFFICIENT` | The key is valid and in service. It simply doesn't carry a scope covering the operation you called. | Add the scope named in `requiredScope` to the key at **/ApiKeys**, or call the operation with a key that already holds it. The change takes effect on the next request. |

Three things follow from it being a 403 rather than a 401:

- **Don't go looking at the credential.** Nothing is wrong with it. Rotating or replacing the key won't help, and a client that treats this like a 401 will loop.
- **The response names the scope it wanted, and only that.** It never lists the scopes the key does hold, because you can read those off the key itself.
- **An unscoped key never sees this code.** If you haven't restricted a key, it can't be refused for a scope.

### When the key's owner lacks the permission

A key can still be refused by the ordinary permission checks after the scope gate lets it through, because scopes narrow and never widen. That refusal is also **HTTP 403**, in the same shape, with the platform's authorization code instead of a key code:

```json
{
  "error": "Forbidden",
  "code": "Volo.Authorization:010002",
  "message": "You are not authorized to perform this operation.",
  "correlationId": "4f1c2a9e8b7d4e3fa6c5b2d1e0f9a8b7"
}
```

| Code | What happened | What to do |
|------|---------------|------------|
| `Volo.Authorization:010002` | The key is valid, in service, and scoped for the operation, but the account that owns it doesn't hold the permission the operation requires. | Check what the key's owning account is allowed to do. Each operation in the reference lists the permissions it needs. Have the permission granted to the owner's role, or call the operation with a key owned by an account that already holds it. |

Branch on `code`, not on the status alone: it's what tells a permission refusal apart from a scope refusal on the same route. `correlationId` is the request's correlation id, the same value the `X-Correlation-Id` response header carries. Quote it when you contact support about a specific refusal. It's omitted when the request has no usable id.

### Routes that need a signed-in user

A few routes end in `self`, and they behave differently from every other route in the API. `self` means the
merchant the signed-in user belongs to, and that association is written into the session when a person
signs in interactively. An API key carries no such association, so these routes can't serve one:

```http
GET /api/merchants/self/custom-fields
```

```json
{
  "error": {
    "code": "Merchants:NoCurrentMerchant",
    "message": "No merchant is associated with the current user. The self-service routes resolve the merchant from the signed-in user, so an API key cannot reach them. Read these settings from the merchant route that takes a merchant id in the path; updating them there needs the merchant update permission."
  }
}
```

The failure is **HTTP 400**, not a 401 or a 403, and it arrives after the key has already authenticated
and cleared its scope check. Adding a scope, widening a permission, or rotating the key changes nothing.

The route that takes the merchant id in the path carries the same settings, so that's where to go
instead:

| Instead of | Call | Permission needed |
|------------|------|-------------------|
| `GET /api/merchants/self/custom-fields` | `GET /api/merchants/{id}`, read `customFields` | None beyond authentication |
| `GET /api/merchants/self/order-data-defaults` | `GET /api/merchants/{id}`, read `processing.orderDataDefaults` | None beyond authentication |
| `PUT /api/merchants/self/custom-fields` | `PUT /api/merchants/{id}` with `customFields` set | `Merchants.Merchants.Update` |
| `PUT /api/merchants/self/order-data-defaults` | `PUT /api/merchants/{id}` with `processing.orderDataDefaults` set | `Merchants.Merchants.Update` |

Two things gate the fallback, and both are worth checking before you build against it. First, the whole
`/api/merchants` surface sits in the `admin` scope, so a key you have restricted needs that scope to
reach these routes at all. An unscoped key is unaffected. Second, **writing needs a permission that
reading doesn't**. `PUT /api/merchants/{id}` requires `Merchants.Merchants.Update`, and the built-in
Merchant role doesn't hold that permission: it carries the self-service settings permissions instead,
which is precisely what the `self` routes ask for. So a key owned by a merchant account can read these
settings over the API but can't write them by any route. A key owned by a reseller or an administrator
account holds the update permission and can do both.

If you need a merchant-owned key to change its own custom fields or order data defaults, raise it with
your integration contact rather than working around it: the permission is the constraint, and it's
granted on the owning account, not on the key.

The `self` routes exist for the merchant-facing screens in the portal, where a user is always present.
Server-to-server integrations should address merchants by id throughout: the id is stable, it's on
every merchant record you already read, and it keeps a single integration able to serve more than one
merchant.

## Bearer tokens

Where an API key doesn't fit, Syntch also issues OAuth 2.0 access tokens from the token endpoint at `/connect/token`. Use this when the caller is a person rather than a service, or when your platform already speaks OAuth:

- An **interactive application** that signs users in and calls the API on their behalf uses the authorization code flow, and renews with the refresh token grant.
- A **confidential server-side client** that acts as itself, with no user present, uses the client credentials grant.
- A **trusted first-party client** that collects credentials directly uses the resource owner password grant, with refresh tokens for renewal.

A token authorizes exactly what its subject is permitted to do, the same way an API key does, so nothing downstream changes based on which credential you presented. Send the token as `Authorization: Bearer <token>`.

Your integration contact issues the client registration for the flow you need. For a straightforward server-to-server integration, an API key is the shorter path and the one to reach for first.

## Make a create idempotent

A payment request that times out in transit leaves you with a real question: did it charge? Idempotency answers it. Set `idempotencyKey` on the transaction create request body to a value you generate and can reproduce on retry:

```json
{
  "idempotencyKey": "order-48213-attempt-1",
  "transactionType": "Sale",
  "invoiceData": {
    "amounts": { "base": 49.00 }
  }
}
```

It's a property of the request body, so it travels with the payload rather than in a header.

### The create response tells you what the key did

Every transaction create response carries an `idempotencyStatus` field reporting what idempotency actually did to that request. Read it rather than inferring anything from the 2xx:

| `idempotencyStatus` | What it means |
| --- | --- |
| `NotRequested` | You sent no key. A repeat send will charge again. |
| `KeyIgnored` | You sent a key, but deduplication isn't enabled for this merchant, so the key had no deduplication effect. A repeat send will charge again. |
| `KeyAccepted` | You sent a key and deduplication is enabled. This is the original create, and the key now protects it. |
| `Replayed` | Your key matched a create that had already completed. Nothing was charged; the body is that original transaction. |

A single check for `KeyIgnored` on your first call against a new merchant is the fastest way to confirm your integration is actually protected.

The field is populated on create responses only. It's absent (null) when you read a transaction back later, because it describes a request rather than anything stored on the transaction.

### Turn deduplication on

Deduplication is off until it's switched on for your merchant, and your integration contact can arrange it. It's switched on in one of two ways: directly, on the merchant's **Processing** settings, or by a default your provider sets across its whole portfolio, which applies to every merchant that hasn't set the value itself. A value set on the merchant always wins, so a merchant switched on individually stays on under a portfolio default of off, and a merchant switched off individually stays off under a portfolio default of on.

Once deduplication is on for your merchant, the key does three things:

- **A repeat is replayed, not recharged.** Send the same key for the same merchant again and you get the original transaction back. The window is **48 hours** from the original create; past that, the same key is treated as a fresh request and will charge.
- **A retry that arrives while the original is still running is told so.** Rather than holding your request open or charging twice, Syntch answers immediately with an "already being processed" rejection. Retry shortly, or look the transaction up by its key.
- **The key is validated.** Up to 128 characters, made of letters, digits, and the characters `.`, `_`, `:`, and `-`.

Two rules to build into your key generator:

- **Make the key specific to the attempt you want deduplicated.** An order id is a good basis; a timestamp or a fresh GUID per retry isn't, because a retry would carry a different key and charge again.
- **Don't start a key with `rb:`, `inv-charge:`, or `inv-installment:`.** Those prefixes are reserved for the scheduled charges recurring billing and invoicing generate for themselves, and they're rejected on every create. Prefix your keys with something of your own.

**While deduplication is off, a key you send is accepted but does nothing.** The create returns a normal 2xx, the key is stored on the transaction, and it's not validated and not deduplicated: a repeat send charges again. A successful response is therefore not confirmation that deduplication is active, which is the one thing to be careful about here. The response says so explicitly: `idempotencyStatus` comes back as `KeyIgnored`. Treat that as a configuration problem to raise with your integration contact, not as a protected charge.

Whatever the deduplication setting, the key you send is stored with the transaction, and a lookup-by-idempotency-key operation returns the transaction a given key produced for a merchant. That lookup is worth wiring into your retry path regardless: before re-sending after a timeout, ask whether the key already produced a transaction.

## Rate limits and the 429 response

Requests are rate limited per API key, and the limits that apply are tuned per deployment and per account rather than published as a fixed number. Design for the response instead of for a specific ceiling and your integration stays correct wherever it runs.

The short version: a limited request comes back as **HTTP 429** with a `Retry-After` header in whole seconds and an `application/problem+json` body. When a limit measures a request, the response carries `X-RateLimit-Limit` and `X-RateLimit-Remaining`, so you can see where you stand without waiting for a rejection. They arrive together or not at all, and their absence means no limit measured the request rather than an allowance of zero. Wait at least the stated interval, back off exponentially with jitter past the first retry, and pair retries of a create with an idempotency key.

The [rate limits reference](/docs/rate-limits) states that response in full, and needs no account to read. [Rate limits and your allowance](/help/guides/rate-limiting) covers your side of it: where to see your own current limits and usage in the portal, how to find the calls that were refused, and how to pace an integration so it stays clear of its limits.

## Timestamps

Every instant Syntch stores and returns is **UTC**. That part is simple and never varies. The string form is what to be careful with, because it differs across the API: some fields serialize a UTC instant with a `Z` suffix, others with an explicit `+00:00` offset. Both mean the same moment.

So two rules cover every case:

- **Always send an explicit offset.** Write `2026-08-06T14:30:00Z` or `2026-08-06T14:30:00+00:00`, never a bare `2026-08-06T14:30:00`. A value with no offset leaves the reading open to interpretation, and stating the offset removes the question entirely.
- **Always parse with an offset-aware type.** Use `DateTimeOffset` in .NET, an aware `datetime` in Python, or `OffsetDateTime` in Java. Parsing into a naive local type is where a correct payload turns into a wrong hour.

Anything you show a person should be converted from UTC at the point of display, using that person's timezone. [How timezones are handled](/help/guides/how-timezones-are-handled) covers the whole model, including what happens in exports and generated documents.

## Move an existing v1 integration

If you already have code written against the previous generation of the API, a compatibility surface speaks the v1 contract, so an existing integration keeps working without being rewritten. It's the supported path for v1 code, and you can adopt the current API for new work at your own pace.

What to expect from it:

- **v1 property naming and enums.** Properties come back PascalCase, and enum values are strings, exactly as v1 published them.
- **v1 authentication.** Callers authenticate with `POST /api/Authenticate` and use the token it returns, rather than with an API key.
- **A decline is a normal outcome, not an error.** A refused payment comes back in the standard v1 transaction envelope with the outcome in `ResultCode` and `ResultText`, the same envelope an approval uses. Branch on the result code, not on the HTTP status alone. The current API answers a refusal the same way, and [Understanding declines and rejections](/help/guides/understanding-declines-and-rejections) covers how to read one: the three result fields, the refusal families, which ones leave a hold, and what you can retry.

New integrations should target the current API described above: it's the one the in-app reference documents, and it's where new capability lands.

## See also

- [Webhook integration](/help/guides/webhook-integration) for receiving platform events at your own endpoint once you are making calls, including the signature scheme and deduplication.
- [The embedded payments SDK](/help/guides/embedded-payments-sdk) for collecting card details in your own page without the card data reaching your servers.
- [Reusing a stored payment method with payment tokens](/help/guides/reusing-saved-cards) for charging a stored card again from your own code.

## See also

- [All documentation](https://devportal-simpay-sbx.winkpg.io/llms.txt): the machine-readable index of every public page on this site.
