# Test helpers API

> Send your test webhook endpoints a payment, refund, dispute, invoice or subscription event on demand, with an ak_test_ key. Never with a live key.

Source: https://anytime-capital.com/docs/test-helpers · Markdown: https://anytime-capital.com/docs/test-helpers.md · All docs: https://anytime-capital.com/docs/llms-full.txt

Test mode only. With a test key (`ak_test_…`), send your test webhook endpoints one event of a type you choose, built from your own test data with the same shape, signature and delivery as the real event, and `livemode: false`. Nothing else changes and no money moves. A live key gets `403 TEST_MODE_ONLY` and nothing is read. Types: `payment.succeeded`, `payment.failed`, `refund.created`, `dispute.opened`, `invoice.paid`, `subscription.updated`. See [Test mode](https://anytime-capital.com/docs/test-mode.md#test-your-integration) for an example of each. AI assistants can send one too, with the [MCP server](https://anytime-capital.com/docs/build-with-ai.md)'s `send_test_event` tool and the same test key.

| Topic | Detail |
| --- | --- |
| Endpoints | [`POST /api/v1/test-helpers/events`](#create-test-event) |
| Key permissions | `webhook_endpoints:write` |
| Samples | curl; Node.js 18+ with the built-in `fetch` (in an ES module); the [Node SDK](https://anytime-capital.com/docs/node-sdk.md) (preview: not on npm yet); Python with `requests`. |
| Every request can also answer | 401 `UNAUTHORIZED`, 401 `API_KEY_EXPIRED`, 403 `NOT_ENROLLED`, 403 `PERMISSION_DENIED`, 403 `IP_NOT_ALLOWED`, 429 `RATE_LIMITED`, 500 `INTERNAL_ERROR`. See [Error codes](https://anytime-capital.com/docs/error-codes.md). |

## The test event object

What the simulator sent: the event's id (the same `evt_…` your endpoints receive), its type, the test object it is about and how many of your endpoints it was queued for.

```json
{
  "object": "test_event",
  "id": "evt_9b2c4d6e8f0a1b3c5d7e9f1a2b4c6d8e",
  "type": "payment.succeeded",
  "livemode": false,
  "objectId": "pay_7Hq2LmX9cR4tVb",
  "deliveries": 1,
  "sample": false
}
```

| Field | Type | Description |
| --- | --- | --- |
| `object` | string | The type of object. Always `test_event`. |
| `id` | string | The event's id, `evt_…`, as your endpoints receive it. |
| `type` | string | The event sent. One of `payment.succeeded`, `payment.failed`, `refund.created`, `dispute.opened`, `invoice.paid`, `subscription.updated`. |
| `livemode` | boolean | Always false: test events never carry live data. Always `false`. |
| `objectId` | string | The test object the event carries as `data`. |
| `deliveries` | integer | Deliveries queued: one per enabled endpoint that takes this event (0 when none does). |
| `sample` | boolean | True when the object is from **Load sample data** (ids `pay_sample…`). |

## Endpoints

### Send a test webhook event

`POST /api/v1/test-helpers/events`

Permission: `webhook_endpoints:write`. Supports `Idempotency-Key`.

Test mode only: needs an `ak_test_` key (a live key gets 403 TEST_MODE_ONLY). Sends your test webhook endpoints one event — payment.succeeded, payment.failed, refund.created, dispute.opened, invoice.paid or subscription.updated — marked `livemode: false`, built from one of your test objects (`objectId`, or the newest that fits) exactly as the real event carries it. Nothing else changes and no money moves. 409 NOTHING_TO_SEND when you have no test object of that kind yet. With an `ak_test_` key only. Leave out `objectId` to use your newest test object that fits.

**Headers**

| Header | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | no | Recommended. A unique key such as a UUID: a retry with the same key within 24 hours gets the first response back instead of acting twice. See [Idempotency](https://anytime-capital.com/docs/idempotency.md). |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | string | yes | The event to send. One of `payment.succeeded`, `payment.failed`, `refund.created`, `dispute.opened`, `invoice.paid`, `subscription.updated`. |
| `objectId` | string | no | The test object it is about (`pay_…`, `re_…`, `inv_…`, `sub_…`); left out, the newest that fits. Up to 64 characters. |

**Request (curl)**

```bash
curl -X POST https://anytime-capital.com/api/v1/test-helpers/events \
  -H "Authorization: Bearer $ANYTIME_API_KEY" \
  -H "Idempotency-Key: test-payment-succeeded-1" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "payment.succeeded",
    "objectId": "pay_7Hq2LmX9cR4tVb"
  }'
```

**Request (Node.js)**

```javascript
const res = await fetch("https://anytime-capital.com/api/v1/test-helpers/events", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ANYTIME_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "test-payment-succeeded-1",
  },
  body: JSON.stringify({ type: "payment.succeeded", objectId: "pay_7Hq2LmX9cR4tVb" }),
});
const testEvent = await res.json();
if (!res.ok) throw new Error(`${testEvent.error.code}: ${testEvent.error.message}`);
```

**Request (Python)**

```python
import os

import requests

res = requests.post(
    "https://anytime-capital.com/api/v1/test-helpers/events",
    headers={
        "Authorization": f"Bearer {os.environ['ANYTIME_API_KEY']}",
        "Idempotency-Key": "test-payment-succeeded-1",
    },
    json={"type": "payment.succeeded", "objectId": "pay_7Hq2LmX9cR4tVb"},
    timeout=30,
)
testEvent = res.json()
if not res.ok:
    raise RuntimeError(f"{testEvent['error']['code']}: {testEvent['error']['message']}")
```

**Response** (201)

```json
{
  "object": "test_event",
  "id": "evt_9b2c4d6e8f0a1b3c5d7e9f1a2b4c6d8e",
  "type": "payment.succeeded",
  "livemode": false,
  "objectId": "pay_7Hq2LmX9cR4tVb",
  "deliveries": 1,
  "sample": false
}
```

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 400 | `INVALID_JSON` | The body isn't valid JSON, or couldn't be read. An empty body reads as `{}`. Send `Content-Type: application/json` and a JSON object. |
| 400 | `INVALID_INPUT` | A field, filter, `limit` or `cursor` is missing or invalid. The message starts with the field's name, as in `amountCents: …`, or reads `Invalid cursor.` Fix the named field. Don't retry the same request unchanged. |
| 400 | `INVALID_IDEMPOTENCY_KEY` | The `Idempotency-Key` header is empty, longer than 255 characters, or has characters other than printable ASCII. Use a UUID. See [Idempotency](https://anytime-capital.com/docs/idempotency.md). |
| 403 | `TEST_MODE_ONLY` | A test helper was called with a live key. Test helpers never touch live data. Use a test key (`ak_test_…`): turn on **Test mode** in the dashboard and create one in **Settings → Developers**. |
| 404 | `NOT_FOUND` | No such object on your business (an id from another business reads the same), or no such endpoint under `/api/v1`, which still answers in JSON. Check the id and the path. Ids are case-sensitive. |
| 409 | `NOTHING_TO_SEND` | You have no test object of the kind this event is about yet (no paid test payment for `payment.succeeded`, no test invoice for `invoice.paid`…). Make one in test mode first (pay a test link with a test card), or choose **Load sample data** in the test mode bar. |
| 422 | `IDEMPOTENCY_KEY_REUSED` | The `Idempotency-Key` was used in the last 24 hours with a different body or endpoint. Use a new key for a new request; reuse a key only to retry the same request. |
| 503 | `NOT_CONFIGURED` | The part of Anytime Capital Payments this needs isn't available right now. Retry later, with the same `Idempotency-Key` on a write. |

---

Anytime Capital is a fintech company, not a bank. Payment processing, banking and payout services are provided by Anytime Capital's licensed partners. Rates shown are for US card payments; international cards, currency conversion and disputes may carry additional fees shown in your dashboard before you accept a payment.
