Download OpenAPI specification:
Test-mode EUR payment API for Epita marketplace teams.
EpitaCard is a Stripe-shaped payment provider. It supports one merchant per
team, EUR integer minor units, synchronous payment outcomes, asynchronous
signed events, and a same-origin merchant dashboard. Every resource is test
mode only and carries livemode: false.
| Property | Value |
|---|---|
| API version | 2026-08-01 |
| Base URL | / — Same-origin HTTPS proxy; local development uses port 9081. |
| Currency | eur only |
| Amounts | integer minor units (cents), maximum 9007199254740991 |
| Mode | test only; every resource carries livemode: false |
Amounts are always integers. There is no decimal, no second currency, and no way to move this account to live mode.
Your team owns one merchant and two keys.
| Key | Use |
|---|---|
sk_test_… |
the API credential; send it on every /v1 request |
pk_test_… |
a public identifier only — it never authorises a /v1 request |
The secret key is accepted two ways. As a bearer token:
Authorization: Bearer sk_test_tNN_…
Or as HTTP Basic, with the secret key as the username and an empty password —
which is what most clients produce from a user: credential pair:
Authorization: Basic <base64("sk_test_tNN_…:")>
A secret key is shown once, when it is issued and when it is rotated, and is stored only as a hash afterwards. Nobody can recover it for you; rotate to get a new one, which invalidates the old one immediately.
The merchant dashboard signs in with a browser session cookie. That session is
not an API credential and cannot create or confirm a payment. If a session
cookie rides along with an Authorization header the header wins, and a
header and cookie belonging to different merchants are rejected with
401 credential_conflict.
A payment intent starts at requires_payment_method.
| Status | Terminal | Event emitted |
|---|---|---|
requires_payment_method |
no | — |
processing |
no | payment_intent.processing |
succeeded |
yes | payment_intent.succeeded |
failed |
yes | payment_intent.failed |
| From | To | Trigger |
|---|---|---|
requires_payment_method |
processing |
confirmation |
processing |
succeeded |
terminal_outcome_success |
processing |
failed |
terminal_outcome_failure |
processing |
failed |
processing_reclaim_timeout |
Confirming an intent writes processing before it attempts any terminal
status, then resolves to one. Whether a concurrent read sees that marker while
the call is still running depends on Idempotency-Key: without the header the
intent is readable at processing straight away; with it the intent
keeps the state it had — or does not exist yet, when the create is what is being
confirmed — until the call returns. Either way a client timeout never means the
payment did not happen — re-read the intent. Confirming an intent that already
left
requires_payment_method returns
400
invalid_request.
There is no cancellation and no refund. A payment that must not stand is a business problem on your side, not an API call.
Pass one of these tokens as payment_method. Any other card number is
rejected with 400 invalid_request.
payment_method |
Result | Payment intent status | Error code | HTTP |
|---|---|---|---|---|
pm_card_4242424242424242 |
success | succeeded |
— | 201 |
pm_card_4000000000000002 |
decline | failed |
card_declined |
402 |
pm_card_4000000000009995 |
decline | failed |
insufficient_funds |
402 |
pm_card_4000000000000069 |
decline | failed |
expired_card |
402 |
pm_card_4000000000000127 |
decline | failed |
incorrect_cvc |
402 |
pm_card_4000000000000119 |
processing failure | failed |
processing_error |
402 |
| Code | Type | HTTP | Retriable |
|---|---|---|---|
invalid_request |
invalid_request_error |
400 | no — terminal |
invalid_api_key |
authentication_error |
401 | no — terminal |
credential_conflict |
authentication_error |
401 | no — terminal |
card_declined |
card_error |
402 | no — terminal |
insufficient_funds |
card_error |
402 | no — terminal |
expired_card |
card_error |
402 | no — terminal |
incorrect_cvc |
card_error |
402 | no — terminal |
processing_error |
card_error |
402 | yes |
resource_missing |
invalid_request_error |
404 | no — terminal |
idempotency_error |
idempotency_error |
409 | no — terminal |
order_amount_mismatch |
invalid_request_error |
409 | no — terminal |
rate_limited |
rate_limit_error |
429 | yes |
api_error |
api_error |
500 | yes |
service_unavailable |
api_error |
503 | yes |
Only processing_error, rate_limited, api_error and service_unavailable are worth
retrying. Retrying a terminal code produces the same answer.
Retry with the same order_id. That is what stops a retry from charging your
buyer twice.
Two independent protections.
1. Idempotency-Key header. Supported on:
POST /v1/payment_intentsPOST /v1/payment_intents/{id}/confirmA repeat of the same request with the same key replays the stored response and
adds Idempotent-Replayed: true. Requests are matched on
method, request_path, canonical_body, so
the same key sent with a different body, or against a different PaymentIntent,
returns 409 idempotency_error. A stored response is kept only for status
200, 201, 402, 409; status
400, 404, 500 does not consume the key, so a
retry gets a fresh attempt. An absent body and {} are the same request.
GET requests ignore the header. These routes reject
it with 400:
POST /v1/webhook_endpointsPATCH /v1/webhook_endpoints/{id}POST /v1/api_keys/rotate2. (merchant, order_id) uniqueness. This one needs no header, which
makes it the protection that covers a retry after a network timeout. Re-creating
an intent for an order_id you already used returns the existing intent with
200 when the amount matches, and 409 order_amount_mismatch when it does
not.
payment_intents, events, webhook_endpoints, webhook_deliveries are ordered by
created DESC then id DESC. Pass the
last ID you received as starting_after to get the next page; the server
selects rows where created < cursor.created OR (created = cursor.created AND id < cursor.id). Every list response carries
has_more and next_starting_after.
Register an HTTPS endpoint you control with POST /v1/webhook_endpoints and
keep the whsec_…
signing secret it returns. There are exactly 3 event types:
payment_intent.succeededpayment_intent.failedpayment_intent.processingDelivery is at least once with bounded retries, so de-duplicate on event.id.
Delivery never changes or delays the synchronous API response you already got.
Every delivery carries an EpitaCard-Signature header:
EpitaCard-Signature: t=1786900201,v1=0000000000000000000000000000000000000000000000000000000000000000
t=<unix>,v1=<hex hmac_sha256(secret, timestamp + "." + raw_body)>.
Reject timestamps outside a five-minute window and compare decoded MAC
bytes in constant time.
Read the raw body before decoding it — re-serialised JSON does not produce the same MAC:
func verifyEpitaCardSignature(secret, header string, body []byte, now time.Time) error {
var timestampText, signatureHex string
for _, part := range strings.Split(header, ",") {
key, value, ok := strings.Cut(strings.TrimSpace(part), "=")
if !ok {
continue
}
switch key {
case "t":
timestampText = value
case "v1":
signatureHex = value
}
}
timestamp, err := strconv.ParseInt(timestampText, 10, 64)
if err != nil {
return fmt.Errorf("invalid signature timestamp: %w", err)
}
delta := now.Unix() - timestamp
if delta < -300 || delta > 300 {
return errors.New("signature timestamp outside five-minute window")
}
received, err := hex.DecodeString(signatureHex)
if err != nil {
return fmt.Errorf("invalid v1 signature: %w", err)
}
mac := hmac.New(sha256.New, []byte(secret))
_, _ = io.WriteString(mac, timestampText+".")
_, _ = mac.Write(body)
if !hmac.Equal(received, mac.Sum(nil)) {
return errors.New("signature mismatch")
}
return nil
}
hmac.Equal is the constant-time comparison; == on the hex strings leaks
timing. Verify before unmarshalling and before any side effect.
Creates an intent in EUR cents. Supplying payment_method immediately
confirms it: confirmation writes processing and its event first, then
one terminal result. Omitting the field returns
requires_payment_method. Secret-key authentication is mandatory.
Header idempotency is evaluated before (merchant, order_id) uniqueness.
Confirmation reserves aggregate merchant headroom before processing and
rejects a safe-integer-ceiling breach with 400 invalid_request.
Sending Idempotency-Key changes when the intent becomes readable.
Without the header the processing intent is durable as soon as it is
written, so a concurrent GET made while the call is still running
already returns it at processing — which is why a client that times
out still finds its payment. With the header nothing is durable until
the call returns: a concurrent GET returns 404 until then, and the
intent appears only once it does. What it appears as depends on the
status. On 200, 201, 402 or 409 the intent is in its final state and
the key holds the response it will replay. On 400, 404 or 500 no
response is stored and the key is released for a fresh attempt, and the
intent can be left readable at processing with no charge — resolved
later like any other unfinished processing intent. Either way, once
the call has returned the intent is readable and a timeout never means
the payment did not happen.
| Idempotency-Key | string [ 1 .. 255 ] characters Example: billing-order-00042 De-duplicates this payment mutation for 24 hours. The fingerprint covers the method, the concrete request path and the canonical JSON body, so a key reused against a different PaymentIntent returns 409 instead of replaying the first one. A matching fingerprint replays the stored response. |
| amount required | integer <int64> (Amount) [ 1 .. 9007199254740991 ] EUR integer minor units (cents), capped at the JSON safe-integer maximum so RFC 8785 idempotency fingerprints preserve exact amounts. |
| order_id required | string [ 1 .. 255 ] characters |
| payment_method | string^pm_card_[0-9]{16}$ |
| description | string or null <= 500 characters |
| receipt_email | string or null <email> <= 320 characters |
| buyer_name | string or null <= 255 characters |
object (Metadata) <= 50 properties |
{- "amount": 1999,
- "order_id": "order_2026_00042",
- "payment_method": "pm_card_4242424242424242",
- "description": "Marché Nord order 42",
- "receipt_email": "buyer@example.test",
- "buyer_name": "Alice Martin",
- "metadata": {
- "cart_id": "cart_00042"
}
}{- "id": "pi_EXAMPLE_SUCCEEDED",
- "object": "payment_intent",
- "livemode": false,
- "amount": 1999,
- "currency": "eur",
- "status": "succeeded",
- "order_id": "order_2026_00042",
- "created": 1786900000,
- "description": "Marché Nord order 42",
- "receipt_email": "buyer@example.test",
- "buyer_name": "Alice Martin",
- "statement_descriptor": "EPITACARD* MARCHE NORD",
- "metadata": {
- "cart_id": "cart_00042"
}, - "latest_charge": {
- "id": "ch_EXAMPLE_SUCCEEDED",
- "object": "charge",
- "livemode": false,
- "amount": 1999,
- "currency": "eur",
- "status": "succeeded",
- "payment_intent": "pi_EXAMPLE_SUCCEEDED",
- "created": 1786900001,
- "outcome": {
- "type": "authorized",
- "network_status": "approved_by_network",
- "seller_message": "Payment complete."
}, - "payment_method_details": {
- "card": {
- "brand": "visa",
- "last4": "4242",
- "exp_month": 12,
- "exp_year": 2030
}
}, - "statement_descriptor": "EPITACARD* MARCHE NORD",
- "failure_code": null,
- "failure_message": null
}, - "last_payment_error": null
}Returns owner-scoped intents ordered by (created DESC, id DESC) with a
strict tuple cursor. Per-status counts honor q but ignore status,
limit, and cursor so dashboard tabs remain comparable.
| status | string (PaymentIntentStatus) Enum: "requires_payment_method" "processing" "succeeded" "failed" Example: status=failed Restrict results to one lifecycle status. |
| q | string [ 1 .. 255 ] characters Example: q=Alice Matches ID, order ID, buyer name, or amount. Amount accepts integer cents or fixed two-decimal EUR. |
| limit | integer [ 1 .. 100 ] Default: 20 Example: limit=20 Maximum page size. |
| starting_after | string non-empty Example: starting_after=pi_EXAMPLE_SUCCEEDED Last resource ID from the preceding page. The server resolves its
|
{- "object": "list",
- "livemode": false,
- "data": [
- {
- "id": "pi_EXAMPLE_SUCCEEDED",
- "object": "payment_intent",
- "livemode": false,
- "amount": 1999,
- "currency": "eur",
- "status": "succeeded",
- "order_id": "order_2026_00042",
- "created": 1786900000,
- "description": "Marché Nord order 42",
- "receipt_email": "buyer@example.test",
- "buyer_name": "Alice Martin",
- "statement_descriptor": "EPITACARD* MARCHE NORD",
- "metadata": {
- "cart_id": "cart_00042"
}, - "latest_charge": {
- "id": "ch_EXAMPLE_SUCCEEDED",
- "object": "charge",
- "livemode": false,
- "amount": 1999,
- "currency": "eur",
- "status": "succeeded",
- "payment_intent": "pi_EXAMPLE_SUCCEEDED",
- "created": 1786900001,
- "outcome": {
- "type": "authorized",
- "network_status": "approved_by_network",
- "seller_message": "Payment complete."
}, - "payment_method_details": {
- "card": {
- "brand": "visa",
- "last4": "4242",
- "exp_month": 12,
- "exp_year": 2030
}
}, - "statement_descriptor": "EPITACARD* MARCHE NORD",
- "failure_code": null,
- "failure_message": null
}, - "last_payment_error": null
}
], - "has_more": false,
- "next_starting_after": null,
- "counts": {
- "requires_payment_method": 1,
- "processing": 0,
- "succeeded": 12,
- "failed": 2
}
}Returns one owner-scoped PaymentIntent.
| id required | string^pi_[A-Za-z0-9_]+$ Example: pi_EXAMPLE_SUCCEEDED Owner-scoped PaymentIntent ID. |
{- "id": "pi_EXAMPLE_SUCCEEDED",
- "object": "payment_intent",
- "livemode": false,
- "amount": 1999,
- "currency": "eur",
- "status": "succeeded",
- "order_id": "order_2026_00042",
- "created": 1786900000,
- "description": "Marché Nord order 42",
- "receipt_email": "buyer@example.test",
- "buyer_name": "Alice Martin",
- "statement_descriptor": "EPITACARD* MARCHE NORD",
- "metadata": {
- "cart_id": "cart_00042"
}, - "latest_charge": {
- "id": "ch_EXAMPLE_SUCCEEDED",
- "object": "charge",
- "livemode": false,
- "amount": 1999,
- "currency": "eur",
- "status": "succeeded",
- "payment_intent": "pi_EXAMPLE_SUCCEEDED",
- "created": 1786900001,
- "outcome": {
- "type": "authorized",
- "network_status": "approved_by_network",
- "seller_message": "Payment complete."
}, - "payment_method_details": {
- "card": {
- "brand": "visa",
- "last4": "4242",
- "exp_month": 12,
- "exp_year": 2030
}
}, - "statement_descriptor": "EPITACARD* MARCHE NORD",
- "failure_code": null,
- "failure_message": null
}, - "last_payment_error": null
}Confirms a requires_payment_method intent. It first writes processing
and that event, then a terminal result.
Confirming processing, succeeded, or failed returns 400
invalid_request. The body may omit payment_method only when one is
already attached. An omitted body and {} share the same idempotency
fingerprint. Secret-key authentication is mandatory.
Aggregate merchant headroom is reserved before the processing marker.
As on create, Idempotency-Key changes when the new state becomes
readable. Without the header a concurrent GET observes processing
while the call is still running. With the header the intent keeps its
previous status for concurrent readers until the call returns. It then
changes straight to its final state together with the response the key
will replay on 200, 201, 402 or 409. On 400, 404 or 500 nothing is
stored, the key is released for a fresh attempt, and the intent can be
left readable at processing with no charge — resolved later like any
other unfinished processing intent.
| id required | string^pi_[A-Za-z0-9_]+$ Example: pi_EXAMPLE_SUCCEEDED Owner-scoped PaymentIntent ID. |
| Idempotency-Key | string [ 1 .. 255 ] characters Example: billing-order-00042 De-duplicates this payment mutation for 24 hours. The fingerprint covers the method, the concrete request path and the canonical JSON body, so a key reused against a different PaymentIntent returns 409 instead of replaying the first one. A matching fingerprint replays the stored response. |
| payment_method | string^pm_card_[0-9]{16}$ |
{- "payment_method": "pm_card_4242424242424242"
}{- "id": "pi_EXAMPLE_SUCCEEDED",
- "object": "payment_intent",
- "livemode": false,
- "amount": 1999,
- "currency": "eur",
- "status": "succeeded",
- "order_id": "order_2026_00042",
- "created": 1786900000,
- "description": "Marché Nord order 42",
- "receipt_email": "buyer@example.test",
- "buyer_name": "Alice Martin",
- "statement_descriptor": "EPITACARD* MARCHE NORD",
- "metadata": {
- "cart_id": "cart_00042"
}, - "latest_charge": {
- "id": "ch_EXAMPLE_SUCCEEDED",
- "object": "charge",
- "livemode": false,
- "amount": 1999,
- "currency": "eur",
- "status": "succeeded",
- "payment_intent": "pi_EXAMPLE_SUCCEEDED",
- "created": 1786900001,
- "outcome": {
- "type": "authorized",
- "network_status": "approved_by_network",
- "seller_message": "Payment complete."
}, - "payment_method_details": {
- "card": {
- "brand": "visa",
- "last4": "4242",
- "exp_month": 12,
- "exp_year": 2030
}
}, - "statement_descriptor": "EPITACARD* MARCHE NORD",
- "failure_code": null,
- "failure_message": null
}, - "last_payment_error": null
}Returns the merchant's settled EUR cents: the total of every succeeded
charge, summed in exact arbitrary precision. There is one figure and no
withdrawal route, so nothing is ever held back or in flight. An
invariant breach above the JSON safe-integer ceiling returns 500
api_error; values are never capped or truncated.
{- "object": "balance",
- "livemode": false,
- "settled": {
- "amount": 126750,
- "currency": "eur"
}
}EpitaCard sends this request at least once to each enabled, subscribed team endpoint. Verify the signature against the raw request body before decoding JSON, enforce the five-minute replay window, compare MAC bytes in constant time, and de-duplicate by event ID. Delivery outcome never changes the originating synchronous API response.
| EpitaCard-Signature required | string^t=[0-9]+,v1=[a-f0-9]{64}$ Example: t=1786900201,v1=0000000000000000000000000000000000000000000000000000000000000000
|
| id required | string^evt_[A-Za-z0-9_]+$ |
| object required | string Value: "event" |
| livemode required | boolean (Livemode) |
| type required | string (EventType) Enum: "payment_intent.succeeded" "payment_intent.failed" "payment_intent.processing" |
| created required | integer <int64> (UnixTimestamp) >= 0 |
required | object (EventData) |
required | object (EventRequest) |
{- "id": "evt_EXAMPLE_SUCCEEDED",
- "object": "event",
- "livemode": false,
- "type": "payment_intent.succeeded",
- "created": 1786900201,
- "data": {
- "object": {
- "id": "pi_EXAMPLE_SUCCEEDED",
- "object": "payment_intent",
- "livemode": false,
- "amount": 125000,
- "currency": "eur",
- "status": "succeeded",
- "order_id": "order_EXAMPLE_SUCCEEDED",
- "created": 1786900200,
- "description": "Example successful order",
- "receipt_email": "buyer@example.test",
- "buyer_name": "Camille Dupont",
- "statement_descriptor": "EPITACARD* MARCHE NORD",
- "metadata": { },
- "latest_charge": null,
- "last_payment_error": null
}
}, - "request": {
- "id": "req_EXAMPLE_SUCCEEDED",
- "idempotency_key": null
}
}{- "received": true
}Returns immutable owner-scoped events ordered by
(created DESC, id DESC) with a strict tuple cursor.
| limit | integer [ 1 .. 100 ] Default: 20 Example: limit=20 Maximum page size. |
| starting_after | string non-empty Example: starting_after=pi_EXAMPLE_SUCCEEDED Last resource ID from the preceding page. The server resolves its
|
{- "object": "list",
- "livemode": false,
- "data": [
- {
- "id": "evt_EXAMPLE_SUCCEEDED",
- "object": "event",
- "livemode": false,
- "type": "payment_intent.succeeded",
- "created": 1786900002,
- "data": {
- "object": {
- "id": "pi_EXAMPLE_SUCCEEDED",
- "object": "payment_intent",
- "livemode": false,
- "amount": 1999,
- "currency": "eur",
- "status": "succeeded",
- "order_id": "order_2026_00042",
- "created": 1786900000,
- "description": "Marché Nord order 42",
- "receipt_email": "buyer@example.test",
- "buyer_name": "Alice Martin",
- "statement_descriptor": "EPITACARD* MARCHE NORD",
- "metadata": { },
- "latest_charge": {
- "id": "ch_EXAMPLE_SUCCEEDED",
- "object": "charge",
- "livemode": false,
- "amount": 1999,
- "currency": "eur",
- "status": "succeeded",
- "payment_intent": "pi_EXAMPLE_SUCCEEDED",
- "created": 1786900001,
- "outcome": {
- "type": "authorized",
- "network_status": "approved_by_network",
- "seller_message": "Payment complete."
}, - "payment_method_details": {
- "card": {
- "brand": "visa",
- "last4": "4242",
- "exp_month": 12,
- "exp_year": 2030
}
}, - "statement_descriptor": "EPITACARD* MARCHE NORD",
- "failure_code": null,
- "failure_message": null
}, - "last_payment_error": null
}
}, - "request": {
- "id": "req_EXAMPLE_PAYMENT",
- "idempotency_key": "billing-order-00042"
}
}
], - "has_more": false,
- "next_starting_after": null
}Returns an EventDetail read projection that adds the latest delivery to the immutable stored Event. The projection is not the signed webhook body.
| id required | string^evt_[A-Za-z0-9_]+$ Example: evt_EXAMPLE_SUCCEEDED Owner-scoped Event ID. |
{- "id": "evt_EXAMPLE_SUCCEEDED",
- "object": "event",
- "livemode": false,
- "type": "payment_intent.succeeded",
- "created": 1786900201,
- "data": {
- "object": {
- "id": "pi_EXAMPLE_SUCCEEDED",
- "object": "payment_intent",
- "livemode": false,
- "amount": 125000,
- "currency": "eur",
- "status": "succeeded",
- "order_id": "order_EXAMPLE_SUCCEEDED",
- "created": 1786900200,
- "description": "Example successful order",
- "receipt_email": "buyer@example.test",
- "buyer_name": "Camille Dupont",
- "statement_descriptor": "EPITACARD* MARCHE NORD",
- "metadata": { },
- "latest_charge": null,
- "last_payment_error": null
}
}, - "request": {
- "id": "req_EXAMPLE_SUCCEEDED",
- "idempotency_key": null
}, - "latest_delivery": {
- "id": "wd_EXAMPLE_SUCCEEDED",
- "livemode": false,
- "event": "payment_intent.succeeded",
- "object": "pi_EXAMPLE_SUCCEEDED",
- "result": "ok",
- "attempts": 1,
- "sent": 1786900203
}
}Registers an owner-scoped HTTPS destination and returns its recoverable
signing secret. In deployed mode URLs must use HTTPS, resolve only to
public addresses, and pass the no-redirect, validated-IP dialing policy.
Idempotency-Key is rejected with 400. Secret-key authentication is
mandatory.
| Idempotency-Key | string [ 1 .. 255 ] characters Example: unsupported-on-this-route Supplying this header to this mutation returns 400 invalid_request. |
| url required | string <uri> <= 2048 characters |
| enabled | boolean Default: true |
| subscribed_events required | Array of strings (EventType) non-empty unique Items Enum: "payment_intent.succeeded" "payment_intent.failed" "payment_intent.processing" |
{- "enabled": true,
- "subscribed_events": [
- "payment_intent.succeeded",
- "payment_intent.failed",
- "payment_intent.processing"
]
}{- "id": "we_EXAMPLE_TEAM07",
- "object": "webhook_endpoint",
- "livemode": false,
- "enabled": true,
- "subscribed_events": [
- "payment_intent.succeeded",
- "payment_intent.failed",
- "payment_intent.processing"
], - "secret": "whsec_EXAMPLE_NOT_A_REAL_SECRET",
- "created": 1786800000,
- "updated": 1786800000
}Returns owner-scoped endpoints, including secrets, ordered by
(created DESC, id DESC) with a strict tuple cursor.
| limit | integer [ 1 .. 100 ] Default: 20 Example: limit=20 Maximum page size. |
| starting_after | string non-empty Example: starting_after=pi_EXAMPLE_SUCCEEDED Last resource ID from the preceding page. The server resolves its
|
{- "object": "list",
- "livemode": false,
- "data": [
- {
- "id": "we_EXAMPLE_TEAM07",
- "object": "webhook_endpoint",
- "livemode": false,
- "enabled": true,
- "subscribed_events": [
- "payment_intent.succeeded",
- "payment_intent.failed",
- "payment_intent.processing"
], - "secret": "whsec_EXAMPLE_NOT_A_REAL_SECRET",
- "created": 1786800000,
- "updated": 1786800000
}
], - "has_more": false,
- "next_starting_after": null
}Returns one owner-scoped endpoint and its signing secret.
| id required | string^we_[A-Za-z0-9_]+$ Example: we_EXAMPLE_TEAM07 Owner-scoped WebhookEndpoint ID. |
{- "id": "we_EXAMPLE_TEAM07",
- "object": "webhook_endpoint",
- "livemode": false,
- "enabled": true,
- "subscribed_events": [
- "payment_intent.succeeded",
- "payment_intent.failed",
- "payment_intent.processing"
], - "secret": "whsec_EXAMPLE_NOT_A_REAL_SECRET",
- "created": 1786800000,
- "updated": 1786800000
}Updates URL, enabled state, or event subscriptions. Use enabled=false
instead of deleting an endpoint. Changed URLs are revalidated under the
same HTTPS, public-address, no-redirect, validated-IP dialing policy.
Idempotency-Key is rejected with 400. Session calls require CSRF
protection.
| id required | string^we_[A-Za-z0-9_]+$ Example: we_EXAMPLE_TEAM07 Owner-scoped WebhookEndpoint ID. |
| X-CSRF-Token | string >= 16 characters Example: EXAMPLE_CSRF_TOKEN Required only when authentication uses the dashboard cookie. |
| Idempotency-Key | string [ 1 .. 255 ] characters Example: unsupported-on-this-route Supplying this header to this mutation returns 400 invalid_request. |
| url | string <uri> <= 2048 characters |
| enabled | boolean |
| subscribed_events | Array of strings (EventType) non-empty unique Items Enum: "payment_intent.succeeded" "payment_intent.failed" "payment_intent.processing" |
{- "enabled": false
}{- "id": "we_EXAMPLE_TEAM07",
- "object": "webhook_endpoint",
- "livemode": false,
- "enabled": false,
- "subscribed_events": [
- "payment_intent.succeeded",
- "payment_intent.failed",
- "payment_intent.processing"
], - "secret": "whsec_EXAMPLE_NOT_A_REAL_SECRET",
- "created": 1786800000,
- "updated": 1786900300
}Returns owner-scoped Event, Object, Result, Attempts, and Sent data ordered
by (created DESC, id DESC) with a strict tuple cursor.
| id required | string^we_[A-Za-z0-9_]+$ Example: we_EXAMPLE_TEAM07 Owner-scoped WebhookEndpoint ID. |
| limit | integer [ 1 .. 100 ] Default: 20 Example: limit=20 Maximum page size. |
| starting_after | string non-empty Example: starting_after=pi_EXAMPLE_SUCCEEDED Last resource ID from the preceding page. The server resolves its
|
{- "object": "list",
- "livemode": false,
- "data": [
- {
- "id": "wd_EXAMPLE_PAYMENT",
- "livemode": false,
- "event": "payment_intent.succeeded",
- "object": "pi_EXAMPLE_SUCCEEDED",
- "result": "ok",
- "attempts": 1,
- "sent": 1786900003
}
], - "has_more": false,
- "next_starting_after": null
}Returns the publishable key and active secret prefix, never the secret.
{- "object": "api_keys",
- "livemode": false,
- "publishable_key": "pk_test_t07_2Bn8xQ",
- "secret_key_prefix": "sk_test_t07_EXAMPL"
}Returns a replacement secret exactly once and invalidates the old key.
Session calls require CSRF protection. Idempotency-Key returns 400,
because replaying a stored secret would violate one-time disclosure.
| X-CSRF-Token | string >= 16 characters Example: EXAMPLE_CSRF_TOKEN Required only when authentication uses the dashboard cookie. |
| Idempotency-Key | string [ 1 .. 255 ] characters Example: unsupported-on-this-route Supplying this header to this mutation returns 400 invalid_request. |
{ }{- "object": "api_key",
- "livemode": false,
- "publishable_key": "pk_test_t07_2Bn8xQ",
- "secret_key": "sk_test_t07_ABCDEFGHIJKLMNOPQRSTUVWXYZ012345",
- "secret_key_prefix": "sk_test_t07_EXAMPL",
- "created": 1786900400
}Authenticates one merchant account and sets two cookies: the
epitacard_session credential and its readable epitacard_csrf
companion. The response body supplies the same CSRF token used by
same-origin dashboard mutations. A separate limiter keyed by client IP
plus trimmed, Unicode-lowercased email returns 429 with Retry-After.
| email required | string <email> <= 320 characters |
| password required | string [ 1 .. 1024 ] characters |
{- "email": "team07@epita.fr",
- "password": "EXAMPLE_NOT_A_REAL_PASSWORD"
}{- "object": "dashboard_session",
- "livemode": false,
- "csrf_token": "EXAMPLE_CSRF_TOKEN",
- "expires_at": 1786986800
}Returns the owning merchant's newest 200 request-log records.
{- "object": "list",
- "livemode": false,
- "data": [
- {
- "id": "req_EXAMPLE_PAYMENT",
- "object": "request_log",
- "livemode": false,
- "request_id": "req_EXAMPLE_PAYMENT",
- "method": "POST",
- "path": "/v1/payment_intents",
- "status": 201,
- "latency_ms": 48,
- "created": 1786900002
}
], - "limit": 200
}Aggregates the owning merchant's PaymentIntents into the caller's day
buckets in one request, for the dashboard overview chart. Day i counts
intents created at or after day_starts[i] and before day_starts[i+1];
the last day is open-ended. Intents created before the first boundary are
not counted. Days without intents are returned with zero totals.
| day_starts required | string^[0-9]+(,[0-9]+){0,30}$ Example: day_starts=1786579200,1786665600,1786752000 Comma-separated, strictly increasing Unix timestamps in seconds, usually the viewer's local midnights. At most 31 values, none more than a day ahead, the first no more than 32 days ago. |
{- "object": "payment_volume",
- "livemode": false,
- "days": [
- {
- "start": 1786579200,
- "attempted": 41,
- "succeeded_count": 39,
- "succeeded_amount": 812450
}, - {
- "start": 1786665600,
- "attempted": 0,
- "succeeded_count": 0,
- "succeeded_amount": 0
}, - {
- "start": 1786752000,
- "attempted": 12,
- "succeeded_count": 12,
- "succeeded_amount": 201300
}
]
}Public, unauthenticated lookup. Rates are decimal fractions, and reduced-rate keys are lowercase shop category names. The static table currently supports France. This free-tier endpoint can return a temporary failure; its responses must not be cached.
| country required | string^[A-Z]{2}$ Example: country=FR Uppercase ISO 3166-1 alpha-2 country code. |
{- "country": "FR",
- "standard_rate": 0.2,
- "reduced_rates": {
- "books": 0.055
}, - "as_of": "2026-01-01"
}