Instruments
Reusable payment instruments (card, account, wallet) tokenized by NETOPIA Payments.
Concepts, behaviour and examples: Instruments guide.
GET /v1/customers/:id/instruments
List a customer's instruments
Cursor-paginated (limit, starting_after).
- Authentication
- Merchant API key
- Effect
- Read-only.
Path parameters
| Name | Type | Required | Description |
|---|
id | string | yes | Customer ID · 26 chars |
Query parameters
| Name | Type | Required | Description |
|---|
limit | integer | no | Page size (1–100). · ≥ 1, ≤ 100, default 20 |
starting_after | string | no | Object ID; return objects after it (ascending ID order). · 26 chars |
Responses
| Status | Meaning | Body / error codes |
|---|
| 200 | Success. | object |
| 401 | Missing or invalid API key. | authentication_error |
| 404 | Not found, or not yours. | resource_missing |
| 429 | Rate limited; honor retry-after. | rate_limit_exceeded too_many_auth_failures |
| 500 | Server error. | api_error |
Example response
{
"object": "list",
"url": "string",
"has_more": false,
"data": [
{
"object": "instrument",
"id": "01JB9XK3M2R7TQ8N4V6Y0ZPA1C",
"merchant_id": "01JB9XK3M2R7TQ8N4V6Y0ZPA1C",
"customer_id": "01JB9XK3M2R7TQ8N4V6Y0ZPA1C",
"type": "card",
"card": {
"last4": "string",
"brand": "string",
"exp_month": 1,
"exp_year": 1
},
"is_default": false,
"enrollment_verified_at": "2026-10-25T18:00:00.000Z",
"created_at": "2026-10-25T18:00:00.000Z"
}
]
}
POST /v1/instruments
Import an instrument token (mock only)
Raw token import works only with NETOPIA_MODE=mock; remote modes return 400 instrument_enrollment_required — use hosted enrollment (POST /v1/enrollments) instead. The first instrument of a customer becomes the default; is_default: true demotes the previous default.
- Authentication
- Merchant API key
- Effect
- Attaches an instrument; may change the customer's default; emits
instrument.attached.
Headers
| Name | Type | Required | Description |
|---|
Idempotency-Key | string | no | Makes retries safe: same key + body replays the stored response (idempotency-replayed: true); a different body returns 409 idempotency_key_in_use. · ≤ 96 chars |
Request body
| Field | Type | Required | Description |
|---|
customer_id | string | yes | ULID · 26 chars |
netopia_token | string | yes | ≤ 4096 chars |
type | "card" | "account" | "wallet" | no | Hosted enrollment always produces card. · default "card" |
card_last4 | string | no | 4 chars |
card_brand | string | no | ≤ 20 chars |
card_exp_month | integer | no | ≥ 1, ≤ 12 |
card_exp_year | integer | no | ≥ 2000, ≤ 3000 |
is_default | boolean | no | |
Example request
{
"customer_id": "01JB9XK3M2R7TQ8N4V6Y0ZPA1C",
"netopia_token": "string",
"type": "card",
"card_last4": "string",
"card_brand": "string",
"card_exp_month": 1,
"card_exp_year": 2000,
"is_default": false
}
Responses
| Status | Meaning | Body / error codes |
|---|
| 201 | Instrument attached. | Instrument |
| 400 | Remote mode: use hosted enrollment. | instrument_enrollment_required |
| 401 | Missing or invalid API key. | authentication_error |
| 404 | Not found, or not yours. | resource_missing |
| 429 | Rate limited; honor retry-after. | rate_limit_exceeded too_many_auth_failures |
| 500 | Server error. | api_error |
Example response
{
"object": "instrument",
"id": "01JB9XK3M2R7TQ8N4V6Y0ZPA1C",
"merchant_id": "01JB9XK3M2R7TQ8N4V6Y0ZPA1C",
"customer_id": "01JB9XK3M2R7TQ8N4V6Y0ZPA1C",
"type": "card",
"card": {
"last4": "string",
"brand": "string",
"exp_month": 1,
"exp_year": 1
},
"is_default": false,
"enrollment_verified_at": "2026-10-25T18:00:00.000Z",
"created_at": "2026-10-25T18:00:00.000Z"
}
GET /v1/instruments/:id
Retrieve an instrument
- Authentication
- Merchant API key
- Effect
- Read-only.
Path parameters
| Name | Type | Required | Description |
|---|
id | string | yes | Object ID (ULID). · 26 chars |
Responses
| Status | Meaning | Body / error codes |
|---|
| 200 | Success. | Instrument |
| 401 | Missing or invalid API key. | authentication_error |
| 404 | Not found, or not yours. | resource_missing |
| 429 | Rate limited; honor retry-after. | rate_limit_exceeded too_many_auth_failures |
| 500 | Server error. | api_error |
Example response
{
"object": "instrument",
"id": "01JB9XK3M2R7TQ8N4V6Y0ZPA1C",
"merchant_id": "01JB9XK3M2R7TQ8N4V6Y0ZPA1C",
"customer_id": "01JB9XK3M2R7TQ8N4V6Y0ZPA1C",
"type": "card",
"card": {
"last4": "string",
"brand": "string",
"exp_month": 1,
"exp_year": 1
},
"is_default": false,
"enrollment_verified_at": "2026-10-25T18:00:00.000Z",
"created_at": "2026-10-25T18:00:00.000Z"
}
DELETE /v1/instruments/:id
Detach an instrument
Soft delete; past charges keep referencing it. Live subscriptions (trialing, active, past_due, paused) using it switch to the customer's oldest remaining instrument.
- Authentication
- Merchant API key
- Effect
- Detaches the instrument and re-points affected subscriptions; emits
instrument.detached (with replacement_instrument_id).
Path parameters
| Name | Type | Required | Description |
|---|
id | string | yes | Object ID (ULID). · 26 chars |
Headers
| Name | Type | Required | Description |
|---|
Idempotency-Key | string | no | Makes retries safe: same key + body replays the stored response (idempotency-replayed: true); a different body returns 409 idempotency_key_in_use. · ≤ 96 chars |
Responses
| Status | Meaning | Body / error codes |
|---|
| 200 | Success. | DeletedObject |
| 401 | Missing or invalid API key. | authentication_error |
| 404 | Not found, or not yours. | resource_missing |
| 409 | Live subscriptions use it and no replacement exists. | instrument_in_use |
| 429 | Rate limited; honor retry-after. | rate_limit_exceeded too_many_auth_failures |
| 500 | Server error. | api_error |
Example response
{
"object": "string",
"id": "01JB9XK3M2R7TQ8N4V6Y0ZPA1C",
"deleted": true
}