Create a fiat integration
POST /api/v1/partner/customers/{customerId}/fiat-integrations
Required scope:
payments:create.Idempotency-Keyis required.
Creates a new checkout configuration for your customer. Use one per storefront that needs its own return destination.
Like the rest of this resource, it is not gated on payments.fiat_card, so
you can set an integration up while the customer’s KYB is still pending.
Request
curl -X POST https://www.niftipay.com/api/v1/partner/customers/pc-1/fiat-integrations \
-H "Authorization: Bearer <partner_api_key>" \
-H "Idempotency-Key: <uuid>" \
-H "Content-Type: application/json" \
-d '{ "name": "storefront-eu",
"return_url": "https://shop.test/done",
"failure_url": "https://shop.test/oops",
"contact_url": "https://shop.test/support" }'
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | Your label. Max 100 chars. Unique per customer. |
return_url |
string | yes | https only. Where the buyer goes after paying. |
failure_url |
string | no | https only. Where the buyer goes after a decline. |
contact_url |
string | no | https only. Support page shown to the buyer. |
Those four fields are the whole surface
psp, psp_project_id and merchant_webhook_url are not settable, and
sending one is a 400 naming the field — not a silent drop:
{
"error": {
"code": "invalid_request",
"message": "`psp` is not a settable field on a fiat integration. Settable fields are: name, return_url, failure_url, contact_url.",
"request_id": "req-1",
"details": [
{
"field": "psp",
"reason": "unknown_field"
}
]
}
}Two reasons the refusal is loud rather than tolerant:
- Every settable field is a URL a buyer is sent to. A misspelled
returnURLthat was quietly ignored would leave the integration pointing wherever it pointed before, and you would find out from a buyer landing on the wrong page. merchant_webhook_urlis where the merchant’s payment notifications go. A partner able to write a field it cannot read could silently redirect them. Your own events arrive on the partner webhook channel instead.
name over 100 characters is refused, never truncated — a shortened name is a
row you cannot find again by the name you think you wrote.
Response 201
{
"request_id": "req-1",
"api_version": "2026-08-01",
"data": {
"integration": {
"integration_id": "95d92bb0-65a4-462f-ae14-90be85bea223",
"name": "storefront-eu",
"psp": "niftipayp1",
"return_url": "https://shop.test/done",
"failure_url": "https://shop.test/oops",
"contact_url": "https://shop.test/support",
"created_at": "2026-08-27T08:53:29.538Z",
"updated_at": null
}
}
}Idempotency
Required here, and only here on this resource. A retry after a socket timeout
cannot know whether the first attempt landed, and without the key that retry
silently doubles the customer’s integration list — the list a later
POST /fiat-orders then has to pick from by id.
Replaying the same key with the same body replays the original response,
including its request_id. The same key with a different body is
409 idempotency_key_reuse. See Idempotency.
A 400 decided from the request alone (bad JSON, or a field that failed
validation) releases the key, so you can fix the payload and retry with the
same one.
Limits
A customer may hold at most 25 integrations. The 26th is
409 limit_reached. This is a ceiling against a runaway retry loop, not a
product limit — a customer needs one integration per storefront.
Errors
| Status | code | Meaning |
|---|---|---|
| 400 | invalid_request | Unknown field, non-https URL, missing name/return_url, name over 100 chars. |
| 400 | idempotency_key_required | No Idempotency-Key header. |
| 403 | insufficient_scope | Token was not minted with payments:create. |
| 404 | resource_not_found | Customer is not yours / relationship missing. |
| 409 | name_conflict | The customer already has an integration with that name. |
| 409 | limit_reached | The customer already holds 25 integrations. |
| 409 | idempotency_key_reuse | Same key, different body. |
| 503 | temporarily_unavailable | Transient database failure. Retry. |