> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rails.wayex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Developers

> Manage API keys and your developer fee in the console, and wire up webhook subscriptions through the API.

The console is a UI over the same API you can call directly. The **Developers** area is where you manage
your **API keys** and your **developer fee**; **webhook endpoints** are managed through the API and are
documented below.

<Note>
  This page covers the developer surface at a product level. For request and response shapes and the
  full endpoint list, see the [API reference](/api-reference/overview).
</Note>

## API keys

Every API request authenticates with an **API key**. There are two kinds:

| Key                 | Prefix | Use it for                                                                                                              |
| ------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------- |
| **Publishable key** | `pk_…` | Documented non-sensitive read-only calls. Do not use it to expose Treasury or beneficiary data in an untrusted browser. |
| **Secret key**      | `sk_…` | Server-side only. Required for state-changing writes. Never expose it in a browser, mobile app, or public repository.   |

Send your key on every request as a bearer token:

```bash theme={null}
Authorization: Bearer YOUR_API_KEY
```

Or, equivalently, in the `X-Api-Key` header:

```bash theme={null}
X-Api-Key: YOUR_API_KEY
```

### Managing keys

From **Developers → API keys** you can:

* **Create** a key.
* **Rotate** a key — issue a replacement and retire the old one.
* **Revoke** a key — disable it immediately.

A key is bound to one tenant account. Reads and writes cannot select another tenant in a request.
Keep each tenant account's key in a separate secret.

A key's **scope** follows its configured access. Treasury reads require `treasury:read`; funding,
beneficiary, payout, quote, conversion, destination, and withdrawal writes require `treasury:write`.
A request made with an out-of-scope key is rejected.

<Warning>
  Treat your secret key like a password. The full secret value is shown **once**, at creation — copy
  it then and store it securely. If a key is ever exposed, rotate it immediately. Wayex will never
  ask you for your secret key.
</Warning>

## Developer fees

Your **developer fee** is your revenue: a markup you charge your end customers on each conversion, on
top of the rate. Configure it from **Developers → Fees** (or programmatically via `GET` / `PUT`
`/v1/developer-fees`) as a **flat AUD amount**, a **percentage in basis points**, or both, applied per
transfer.

The exchange rate you and your customers see is the **all-in rate Wayex quotes** — Wayex's margin is
inside that rate and is not your revenue. Your developer fee is the only fee surface you configure, and
it works like this:

* **Withheld from each conversion.** When a funded route converts, your fee is deducted from the
  conversion — the end customer receives the net **minus your fee**. The withheld amount is held owed
  to you.
* **Credited against your monthly invoice.** Each billing period, your accrued developer fees appear as
  a credit on your Wayex invoice, netted against the platform fees you owe — see
  [Billing and invoices](/console/activity).
* **Applies going forward.** A saved schedule applies to conversions struck **after** the change; it
  never reprices a conversion already in flight.
* **Capped.** A schedule above the per-transfer caps — **500 basis points** or **A\$100.00 flat** — is
  rejected outright (HTTP `422`), never silently reduced.
* **Never eats a transfer.** If a configured fee would consume an entire conversion, the fee for that
  transfer is reduced to zero and the conversion proceeds normally; the skip is recorded. A fee
  misconfiguration can never strand a customer's funds.

Setting an empty schedule means you charge no developer fee. Updating the schedule is value-affecting:
it requires a secret key (or a console role with write access), and every change is audited.

<Note>
  This developer fee applies to direct customer-route conversions. Treasury pricing and operation
  fees come from the effective settings for the authenticated tenant account.
</Note>

## Webhooks

Webhooks are how your systems learn about things that happen asynchronously — a customer's verification
outcome, a payment route being created, and every conversion a funded route spawns. Webhooks are the
signal that state changed: subscribe an endpoint, verify the event, then fetch the current resource
as the authoritative state.

### Subscribe your endpoint

Register an HTTPS URL on your server with `POST /v1/webhooks`. The request must include the `url`, the
`eventTypes` you want, and a `secret` you choose (minimum 8 characters — use a long random value). That
secret is what Wayex uses to sign every delivery to the endpoint, so generate it yourself and keep your
own copy securely: Wayex stores it encrypted and never returns it in any response. Wayex sends each
event to your URL as an HTTP `POST` with a JSON body.

### Event shape

Every event carries a stable **`eventId`**, a **`type`**, a **`createdAt`** timestamp, and a typed
**`data`** payload. The event types you can receive are:

| Event                         | Fires when                                                                      |
| ----------------------------- | ------------------------------------------------------------------------------- |
| `counterparty.created`        | A counterparty is created.                                                      |
| `counterparty.invited`        | A counterparty is invited to the console.                                       |
| `counterparty.activated`      | A counterparty completes activation.                                            |
| `customer.created`            | A customer is created.                                                          |
| `kyc.updated`                 | A customer's verification status changes (and entitlements update on approval). |
| `payment_route.created`       | A payment route is created.                                                     |
| `transfer.created`            | A funded route spawns a conversion.                                             |
| `transfer.updated`            | A transfer changes state.                                                       |
| `transfer.completed`          | A transfer completes.                                                           |
| `treasury.wallet.updated`     | A Treasury wallet balance or reservation changes.                               |
| `treasury.payin.updated`      | An incoming AUD pay-in changes state.                                           |
| `treasury.deposit.updated`    | An incoming stablecoin deposit changes state.                                   |
| `treasury.payout.updated`     | An AUD payout changes state.                                                    |
| `treasury.withdrawal.updated` | A stablecoin withdrawal changes state.                                          |
| `treasury.conversion.updated` | A Treasury conversion or physical settlement leg changes state.                 |

<Note>
  Transfers are created automatically when a route is funded — Wayex converts at the live rate the
  instant funds land. You never create a transfer yourself, so there is no quote to accept and no
  price to lock; the binding price is struck fresh at funding. The Rates API and the console Rates
  screen are live indicative only and never bind.
</Note>

### Verify the signature

Each delivery is **signed** so you can confirm it genuinely came from Wayex and was not tampered
with. Wayex sends the signature in the **`X-Wayex-Signature`** header, formatted as
`sha256=<hex>`, where `<hex>` is the **HMAC-SHA256** of the **raw request body**, keyed by the signing
secret **you supplied** in the `secret` field when you created the subscription. Wayex stores that
secret encrypted and never returns it in any response, so verification uses the copy you kept.

<Steps>
  <Step title="Read the raw body">
    Capture the exact raw request bytes before any JSON parsing — the signature is computed over the
    raw body, so re-serializing it will change the result.
  </Step>

  <Step title="Compute your own HMAC">`HMAC-SHA256(rawBody, yourSigningSecret)`, hex-encoded.</Step>

  <Step title="Compare in constant time">
    Strip the `sha256=` prefix from the header and compare against your computed hex using a
    constant-time comparison. If they do not match, reject the request with a non-2xx.
  </Step>
</Steps>

```js theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyWayexSignature(rawBody, header, signingSecret) {
  const expected = createHmac('sha256', signingSecret).update(rawBody).digest('hex');
  const received = (header ?? '').replace(/^sha256=/, '');
  const a = Buffer.from(expected);
  const b = Buffer.from(received);
  return a.length === b.length && timingSafeEqual(a, b);
}
```

### Handle deliveries safely

Delivery is **at-least-once** and **unordered**, so build your consumer to tolerate that:

* **Deduplicate** by `eventId` — you may receive the same event more than once.
* **Do not assume ordering** — events can arrive out of sequence. When you need the authoritative state of
  a resource, fetch it from the API rather than inferring it from event order.
* **Respond quickly** with a `2xx` to acknowledge receipt, then do heavier work asynchronously.

### Delivery and retries

Your endpoint must return a `2xx` within **10 seconds** — do heavier work asynchronously. A non-`2xx`
response, a timeout, or **any redirect** counts as a failed attempt: redirects are never followed, so
serve the webhook URL directly over HTTPS at a publicly resolvable address (the URL is re-validated on
every attempt).

A failed delivery is retried with exponential backoff: **up to 8 attempts in total**, spaced roughly
10 seconds, 20 seconds, 40 seconds, 80 seconds, 2.7 minutes, 5.3 minutes, 10.7 minutes, and 21.3 minutes
apart (with jitter), so the full schedule spans about **40 minutes**. After the final attempt the
delivery is marked `dead` and is never retried — reconcile missed events with
`GET /v1/webhooks/{id}/deliveries` and by fetching current resource state.

Alongside `X-Wayex-Signature`, each delivery carries `x-wayex-event-id`, `x-wayex-event-type`,
`x-wayex-delivery-id`, `x-wayex-delivery-attempt`, and `x-wayex-timestamp` headers you can use for
logging and dedupe.

### Delivery logs

Retrieve recent **deliveries** for a subscription with `GET /v1/webhooks/{id}/deliveries` — each
delivery reports the event sent, the attempts made, the HTTP status your endpoint returned, and whether
it is `pending`, `failed` (awaiting a retry), `delivered`, or `dead`. Use it to debug an integration and
confirm events are being received.

<Note>
  Sandbox is fully simulated — no real funds ever move. Production moves real funds. Treat your
  webhook handling as production code in every environment.
</Note>
