Skip to main content

Consents

4 operations under /v1/consents (POST GET): parameters, status codes, refusal bodies, and the same request in curl, HTTP, Python and TypeScript.

13 min read
View MarkdownEdit on GitHub

Base URL: https://api.codespar.dev

Operations below require a Bearer token unless their bar says No credential. See Authentication.

POST /v1/consents

POSThttps://api.codespar.dev/v1/consents

Start a hosted consent

Starts a hosted consent for a consumer mandate. The partner's backend calls this with its API key and gets a one-shot token carrying the intent the consumer is about to authorize (purpose, total and per-transaction caps, currency, mandate TTL and the merchant, withdrawal and DDA allowlists that will be signed into the mandate). The partner composes the URL of the hosted consent page from the token and hands it to the consumer.

The mandate is created server-side when the consent is submitted: the consumer's secret is provisioned, the mandate is signed with it, the funding source and the consent record are written and the token is consumed, in one transaction. The API key never signs — nothing a partner can call WITH A KEY produces a signed mandate.

surface decides who runs that ceremony, and it is fixed here, at creation. hosted (the default): hand consent_url to the consumer and our page does the rest. partner: you render your own screen and call GET /v1/consents/{token} and POST /v1/consents/{token}/submit yourself, both described in this document, and the submit must carry an attestation saying who witnessed the human and when. That block is signed into the mandate, which is what keeps the two surfaces distinguishable in an audit instead of collapsing into an IP address that would be your server's either way.

intent.merchant_allowlist defaults to ["*"], an explicit wildcard bounded by the caps, purpose and expiry; pass concrete Pix keys to narrow it. intent.withdrawal_allowlist and intent.dda_allowlist are deliberately NOT defaulted: absent means the mandate authorizes no cash-out and no DDA registration, which is a different thing from a wildcard, and "*" is refused in both. callback_url, when given, is returned to the hosted page at submit time so the partner's backend can receive the signed mandate.

The token is stamped with the credential's project, and that stamp is what scopes the funding source the consumer's submission later creates.

Requires the consents:write scope.

Request body

FieldTypeRequiredDescription
agent_idstringyes—
callback_urlstring (uri)no—
consumer_email_hintstring (email)no—
intentobjectyes—
surface"hosted" | "partner"no—

Responses

StatusBodyDescription
201objectCreated. The consent is pending until the consumer signs on the hosted page or the token expires.
400objectThe body did not match the schema. details.issues carries the Zod issues; the refusals with their own message are a "*" entry in intent.withdrawal_allowlist and a intent.dda_allowlist entry that is not a CPF (11 digits) or a CNPJ (14 digits).

Response 201

FieldTypeRequiredDescription
consent_urlstringyesThe hosted page for this token, composed from the server's configured base. Hand it to the consumer on the hosted surface. It is returned on partner too, where it is a usable fallback: the same token also answers on the page.
expires_atstringyesISO 8601. The token expires 24 hours after this call; a consumer opening the link after that sees an expired consent and nothing is created. This is the TOKEN's TTL, not the mandate's: the mandate's own TTL is intent.mandate_ttl_seconds, counted from the moment the consumer signs.
surface"hosted" | "partner"yesEcho of the requested surface, which is fixed for this token's life. hosted: the consumer signs on the page at consent_url and the submit must NOT carry an attestation. partner: you render the screen and call GET /v1/consents/{token} and POST /v1/consents/{token}/submit yourself, and the submit MUST carry attestation.
tokenstringyesOne-shot, ctk_-prefixed, unguessable. Put it in the URL of the hosted consent page and hand that URL to the consumer. It is consumed by the consumer's submission and cannot be replayed.
Example request
curl -X POST https://api.codespar.dev/v1/consents \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "agent_id": "agt_0000000000000000",
       "consumer_email_hint": "person@example.com",
       "intent": {
         "purpose": "string",
         "cap_minor": 1,
         "per_tx_cap_minor": 1,
         "currency": "BRL",
         "mandate_ttl_seconds": 0,
         "merchant_allowlist": [
           "*"
         ],
         "merchant_pin_kind": "pix-key",
         "withdrawal_allowlist": [
           "string"
         ],
         "dda_allowlist": [
           "string"
         ],
         "periodic_cap": {
           "window": "day",
           "cap_minor": 1
         },
         "slots": [
           {
             "currency": "BRL",
             "rail": "string",
             "cap_minor": 1,
             "per_tx_cap_minor": 1
           }
         ],
         "display_name": "Example",
         "intent_note": "string",
         "shipping": {}
       },
       "callback_url": "https://example.com/hook",
       "surface": "hosted"
     }'
POST /v1/consents HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "agent_id": "agt_0000000000000000",
  "consumer_email_hint": "person@example.com",
  "intent": {
    "purpose": "string",
    "cap_minor": 1,
    "per_tx_cap_minor": 1,
    "currency": "BRL",
    "mandate_ttl_seconds": 0,
    "merchant_allowlist": [
      "*"
    ],
    "merchant_pin_kind": "pix-key",
    "withdrawal_allowlist": [
      "string"
    ],
    "dda_allowlist": [
      "string"
    ],
    "periodic_cap": {
      "window": "day",
      "cap_minor": 1
    },
    "slots": [
      {
        "currency": "BRL",
        "rail": "string",
        "cap_minor": 1,
        "per_tx_cap_minor": 1
      }
    ],
    "display_name": "Example",
    "intent_note": "string",
    "shipping": {}
  },
  "callback_url": "https://example.com/hook",
  "surface": "hosted"
}
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/consents",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "agent_id": "agt_0000000000000000",
      "consumer_email_hint": "person@example.com",
      "intent": {
        "purpose": "string",
        "cap_minor": 1,
        "per_tx_cap_minor": 1,
        "currency": "BRL",
        "mandate_ttl_seconds": 0,
        "merchant_allowlist": [
          "*"
        ],
        "merchant_pin_kind": "pix-key",
        "withdrawal_allowlist": [
          "string"
        ],
        "dda_allowlist": [
          "string"
        ],
        "periodic_cap": {
          "window": "day",
          "cap_minor": 1
        },
        "slots": [
          {
            "currency": "BRL",
            "rail": "string",
            "cap_minor": 1,
            "per_tx_cap_minor": 1
          }
        ],
        "display_name": "Example",
        "intent_note": "string",
        "shipping": {}
      },
      "callback_url": "https://example.com/hook",
      "surface": "hosted"
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/consents", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "agent_id": "agt_0000000000000000",
    "consumer_email_hint": "person@example.com",
    "intent": {
      "purpose": "string",
      "cap_minor": 1,
      "per_tx_cap_minor": 1,
      "currency": "BRL",
      "mandate_ttl_seconds": 0,
      "merchant_allowlist": [
        "*"
      ],
      "merchant_pin_kind": "pix-key",
      "withdrawal_allowlist": [
        "string"
      ],
      "dda_allowlist": [
        "string"
      ],
      "periodic_cap": {
        "window": "day",
        "cap_minor": 1
      },
      "slots": [
        {
          "currency": "BRL",
          "rail": "string",
          "cap_minor": 1,
          "per_tx_cap_minor": 1
        }
      ],
      "display_name": "Example",
      "intent_note": "string",
      "shipping": {}
    },
    "callback_url": "https://example.com/hook",
    "surface": "hosted"
  }),
});

const data = await res.json();
const result = await cs.api.post("/v1/consents", {
  body: {
    agent_id: "agt_0000000000000000",
    consumer_email_hint: "person@example.com",
    intent: {
      purpose: "string",
      cap_minor: 1,
      per_tx_cap_minor: 1,
      currency: "BRL",
      mandate_ttl_seconds: 0,
      merchant_allowlist: [
        "*"
      ],
      merchant_pin_kind: "pix-key",
      withdrawal_allowlist: [
        "string"
      ],
      dda_allowlist: [
        "string"
      ],
      periodic_cap: {
        window: "day",
        cap_minor: 1
      },
      slots: [
        {
          currency: "BRL",
          rail: "string",
          cap_minor: 1,
          per_tx_cap_minor: 1
        }
      ],
      display_name: "Example",
      intent_note: "string",
      shipping: {}
    },
    callback_url: "https://example.com/hook",
    surface: "hosted"
  }
});
Example response 201
application/json
{
  "token": "string",
  "expires_at": "string",
  "surface": "hosted",
  "consent_url": "https://example.com/hook"
}

POST /v1/consents/init

POSThttps://api.codespar.dev/v1/consents/init
Deprecated

Start a hosted consent

DEPRECATED alias of POST /v1/consents (ent#979), kept for two releases. Same handler, same required scope, same request and same responses; switch the path and nothing else changes. The canonical path is described in this document too.

Starts a hosted consent for a consumer mandate. The partner's backend calls this with its API key and gets a one-shot token carrying the intent the consumer is about to authorize (purpose, total and per-transaction caps, currency, mandate TTL and the merchant, withdrawal and DDA allowlists that will be signed into the mandate). The partner composes the URL of the hosted consent page from the token and hands it to the consumer.

The mandate is created server-side when the consent is submitted: the consumer's secret is provisioned, the mandate is signed with it, the funding source and the consent record are written and the token is consumed, in one transaction. The API key never signs — nothing a partner can call WITH A KEY produces a signed mandate.

surface decides who runs that ceremony, and it is fixed here, at creation. hosted (the default): hand consent_url to the consumer and our page does the rest. partner: you render your own screen and call GET /v1/consents/{token} and POST /v1/consents/{token}/submit yourself, both described in this document, and the submit must carry an attestation saying who witnessed the human and when. That block is signed into the mandate, which is what keeps the two surfaces distinguishable in an audit instead of collapsing into an IP address that would be your server's either way.

intent.merchant_allowlist defaults to ["*"], an explicit wildcard bounded by the caps, purpose and expiry; pass concrete Pix keys to narrow it. intent.withdrawal_allowlist and intent.dda_allowlist are deliberately NOT defaulted: absent means the mandate authorizes no cash-out and no DDA registration, which is a different thing from a wildcard, and "*" is refused in both. callback_url, when given, is returned to the hosted page at submit time so the partner's backend can receive the signed mandate.

The token is stamped with the credential's project, and that stamp is what scopes the funding source the consumer's submission later creates.

Requires the consents:write scope.

Request body

FieldTypeRequiredDescription
agent_idstringyes—
callback_urlstring (uri)no—
consumer_email_hintstring (email)no—
intentobjectyes—
surface"hosted" | "partner"no—

Responses

StatusBodyDescription
201objectCreated. The consent is pending until the consumer signs on the hosted page or the token expires.
400objectThe body did not match the schema. details.issues carries the Zod issues; the refusals with their own message are a "*" entry in intent.withdrawal_allowlist and a intent.dda_allowlist entry that is not a CPF (11 digits) or a CNPJ (14 digits).

Response 201

FieldTypeRequiredDescription
consent_urlstringyesThe hosted page for this token, composed from the server's configured base. Hand it to the consumer on the hosted surface. It is returned on partner too, where it is a usable fallback: the same token also answers on the page.
expires_atstringyesISO 8601. The token expires 24 hours after this call; a consumer opening the link after that sees an expired consent and nothing is created. This is the TOKEN's TTL, not the mandate's: the mandate's own TTL is intent.mandate_ttl_seconds, counted from the moment the consumer signs.
surface"hosted" | "partner"yesEcho of the requested surface, which is fixed for this token's life. hosted: the consumer signs on the page at consent_url and the submit must NOT carry an attestation. partner: you render the screen and call GET /v1/consents/{token} and POST /v1/consents/{token}/submit yourself, and the submit MUST carry attestation.
tokenstringyesOne-shot, ctk_-prefixed, unguessable. Put it in the URL of the hosted consent page and hand that URL to the consumer. It is consumed by the consumer's submission and cannot be replayed.
Example request
curl -X POST https://api.codespar.dev/v1/consents/init \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "agent_id": "agt_0000000000000000",
       "consumer_email_hint": "person@example.com",
       "intent": {
         "purpose": "string",
         "cap_minor": 1,
         "per_tx_cap_minor": 1,
         "currency": "BRL",
         "mandate_ttl_seconds": 0,
         "merchant_allowlist": [
           "*"
         ],
         "merchant_pin_kind": "pix-key",
         "withdrawal_allowlist": [
           "string"
         ],
         "dda_allowlist": [
           "string"
         ],
         "periodic_cap": {
           "window": "day",
           "cap_minor": 1
         },
         "slots": [
           {
             "currency": "BRL",
             "rail": "string",
             "cap_minor": 1,
             "per_tx_cap_minor": 1
           }
         ],
         "display_name": "Example",
         "intent_note": "string",
         "shipping": {}
       },
       "callback_url": "https://example.com/hook",
       "surface": "hosted"
     }'
POST /v1/consents/init HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "agent_id": "agt_0000000000000000",
  "consumer_email_hint": "person@example.com",
  "intent": {
    "purpose": "string",
    "cap_minor": 1,
    "per_tx_cap_minor": 1,
    "currency": "BRL",
    "mandate_ttl_seconds": 0,
    "merchant_allowlist": [
      "*"
    ],
    "merchant_pin_kind": "pix-key",
    "withdrawal_allowlist": [
      "string"
    ],
    "dda_allowlist": [
      "string"
    ],
    "periodic_cap": {
      "window": "day",
      "cap_minor": 1
    },
    "slots": [
      {
        "currency": "BRL",
        "rail": "string",
        "cap_minor": 1,
        "per_tx_cap_minor": 1
      }
    ],
    "display_name": "Example",
    "intent_note": "string",
    "shipping": {}
  },
  "callback_url": "https://example.com/hook",
  "surface": "hosted"
}
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/consents/init",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "agent_id": "agt_0000000000000000",
      "consumer_email_hint": "person@example.com",
      "intent": {
        "purpose": "string",
        "cap_minor": 1,
        "per_tx_cap_minor": 1,
        "currency": "BRL",
        "mandate_ttl_seconds": 0,
        "merchant_allowlist": [
          "*"
        ],
        "merchant_pin_kind": "pix-key",
        "withdrawal_allowlist": [
          "string"
        ],
        "dda_allowlist": [
          "string"
        ],
        "periodic_cap": {
          "window": "day",
          "cap_minor": 1
        },
        "slots": [
          {
            "currency": "BRL",
            "rail": "string",
            "cap_minor": 1,
            "per_tx_cap_minor": 1
          }
        ],
        "display_name": "Example",
        "intent_note": "string",
        "shipping": {}
      },
      "callback_url": "https://example.com/hook",
      "surface": "hosted"
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/consents/init", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "agent_id": "agt_0000000000000000",
    "consumer_email_hint": "person@example.com",
    "intent": {
      "purpose": "string",
      "cap_minor": 1,
      "per_tx_cap_minor": 1,
      "currency": "BRL",
      "mandate_ttl_seconds": 0,
      "merchant_allowlist": [
        "*"
      ],
      "merchant_pin_kind": "pix-key",
      "withdrawal_allowlist": [
        "string"
      ],
      "dda_allowlist": [
        "string"
      ],
      "periodic_cap": {
        "window": "day",
        "cap_minor": 1
      },
      "slots": [
        {
          "currency": "BRL",
          "rail": "string",
          "cap_minor": 1,
          "per_tx_cap_minor": 1
        }
      ],
      "display_name": "Example",
      "intent_note": "string",
      "shipping": {}
    },
    "callback_url": "https://example.com/hook",
    "surface": "hosted"
  }),
});

const data = await res.json();
const result = await cs.api.post("/v1/consents/init", {
  body: {
    agent_id: "agt_0000000000000000",
    consumer_email_hint: "person@example.com",
    intent: {
      purpose: "string",
      cap_minor: 1,
      per_tx_cap_minor: 1,
      currency: "BRL",
      mandate_ttl_seconds: 0,
      merchant_allowlist: [
        "*"
      ],
      merchant_pin_kind: "pix-key",
      withdrawal_allowlist: [
        "string"
      ],
      dda_allowlist: [
        "string"
      ],
      periodic_cap: {
        window: "day",
        cap_minor: 1
      },
      slots: [
        {
          currency: "BRL",
          rail: "string",
          cap_minor: 1,
          per_tx_cap_minor: 1
        }
      ],
      display_name: "Example",
      intent_note: "string",
      shipping: {}
    },
    callback_url: "https://example.com/hook",
    surface: "hosted"
  }
});
Example response 201
application/json
{
  "token": "string",
  "expires_at": "string",
  "surface": "hosted",
  "consent_url": "https://example.com/hook"
}

GET /v1/consents/{token}

GEThttps://api.codespar.dev/v1/consents/{token}
No credential

Read a pending consent

Returns what a human needs to see before authorizing: the org, the agent and the intent that will be signed. No credential — the token in the URL is the authentication, and it is the same token on both surfaces.

The hosted page calls this to render itself. On the partner surface you call it to render your own screen, and surface in the response tells you which you are on.

It never returns a secret, a mandate or a provisioned id: nothing here signs anything. A token that was already used answers 409 and an expired one 410, so the screen can say which happened.

Path parameters

NameTypeRequiredDescription
tokenstringyes—

Responses

StatusBodyDescription
200objectThe consent is pending and these are its human-readable parts.
404objectNo such token.
409objectThe token was already submitted. One-shot by construction (a partial unique index on the pending state), so this is what a replay gets.
410objectThe token expired. Start a new consent.

Response 200

FieldTypeRequiredDescription
agent_idstringyesThe agent the mandate will authorize.
consumer_email_hintstring,nullyesWhat the partner passed at creation, to help the consumer recognize the request.
expires_atstringyesISO 8601. After this the token answers 410.
intentobjectyesThe intent as it was minted: purpose, caps, currency, mandate TTL and the allowlists that will be signed. Show it — this is what the human is authorizing.
org_namestringyesThe org's display name, for the screen. An organization when it cannot be read.
rails_supportedarray of stringyesLegal values for rail on the submit below.
surface"hosted" | "partner"yesThe surface this token was opened for. partner means the submit requires attestation; hosted means it refuses one. Read it before submitting rather than assuming: the org chose it when it created the consent.
tokenstringyes—
Example request
curl -X GET https://api.codespar.dev/v1/consents/{token} \
  -H "Authorization: Bearer $CODESPAR_API_KEY"
GET /v1/consents/{token} HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
import os
import requests

res = requests.get(
    "https://api.codespar.dev/v1/consents/{token}",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/consents/{token}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
  },
});

const data = await res.json();
const result = await cs.api.get("/v1/consents/{token}", {
  path: {
    token: "string"
  }
});
Example response 200
application/json
{
  "token": "string",
  "org_name": "Example",
  "agent_id": "agt_0000000000000000",
  "consumer_email_hint": "person@example.com",
  "intent": {},
  "expires_at": "string",
  "rails_supported": [
    "string"
  ],
  "surface": "hosted"
}

POST /v1/consents/{token}/submit

POSThttps://api.codespar.dev/v1/consents/{token}/submit
No credential

Submit a consent and receive the signed mandate

Consumes the token and returns the signed consumer mandate. In one transaction: the consumer's secret is provisioned if this is their first consent, the funding source is created from provider_token (encrypted through the vault, never stored in the clear), the consent record is written with the IP and user-agent of THIS call, the mandate is signed with the consumer's secret and the token is consumed. No credential: the token is the authentication, and it is one-shot.

WHO may call it is decided by the surface the org chose at creation, and the two are not interchangeable:

  • hosted — the consumer's browser submits from our page. attestation is REFUSED (400 attestation_not_accepted): our page witnessed the act, and the IP and user-agent on the consent record are the consumer's.
  • partner — your server submits. attestation is REQUIRED (400 attestation_required): the IP and user-agent recorded are your server's, so the only statement about the human is yours. It is signed into the mandate as consent_attestation and cannot be edited afterwards without breaking the signature.

attestation.method says how the human authorized: partner_session (an authenticated session in your product), in_person, verified_code (a code YOU issued and verified), partner_biometric (YOU ran a biometric check at the moment of the act), or sandbox_fixture (no human authorized: a CodeSpar test surface signed a sandbox fixture; accepted ONLY for a test project, 403 attestation_sandbox_fixture_not_permitted on a live one). partner_biometric is not a flavour of partner_session: in a dispute a session says someone was logged in and a biometric says a body was present. We capture and verify no biometric ourselves — this records what YOU assert, sealed by the mandate signature so it cannot be edited afterwards; the evidence behind it stays with you. asserted_at is the instant you say they authorized, in Unix seconds. reference is your own record id, [A-Za-z0-9_-]{1,120}, and never personal data — it is joined into the signed string, so the character set is the schema's, not a suggestion.

attestation.evidence (optional) is what you OBSERVED of the act, signed with the rest of the mandate. channel is required and decides which other keys may be present, because a value a channel cannot expose was inferred, and an inferred value inside a signed record reads exactly like an observed one:

  • whatsapp, sms, email, llm_chat — contact, message_id, session_id, provider_ts. These hand you a conversation, not a connection.
  • web_chat — the above plus ip, user_agent, geo. A browser has no install, so no device_id_hash.
  • app — the above plus device_id_hash.
  • other — everything, because we cannot say what a channel we have not named exposes. An auditor reading other knows exactly that.

A key outside its channel's set, or a key the object does not declare, is a 400 attestation_evidence_invalid with details.key naming it — never a silent drop. contact is the e-mail or E.164 phone the consumer acted from; it is hashed server-side into the signed contact_hash and never stored in the clear. device_id_hash you hash yourself (64 hex characters). geo is {country, region, city} and refuses coordinates.

method: "verified_code" additionally REQUIRES evidence.contact to carry a contact verification we ran for this consumer in the last 24h (POST /v1/consumers/{consumerId}/contact-verifications), or the submit is refused with 409 contact_verification_required and the token is left unspent. A rung that is only asserted is not stronger than the one below it.

Path parameters

NameTypeRequiredDescription
tokenstringyes—

Request body

FieldTypeRequiredDescription
attestationobjectno—
consumer_idstringyes—
display_labelstringno—
provider_tokenstringyes—
rail"pix-consent" | "card-token" | "ted-debit-auth" | "usd-ach-debit" | "usdc-onchain"yes—

Responses

StatusBodyDescription
201objectThe mandate is signed and stored, and the token is spent. Store the mandate and its signature: both are presented on every spend.
400objectThe body did not match the schema (invalid_body, with the Zod issues in details.issues), the attestation does not match the token's surface (attestation_required on partner without one, attestation_not_accepted on hosted with one), or the evidence carries a key its channel cannot expose or one the object does not declare (attestation_evidence_invalid, details.key).
403objectattestation.method is sandbox_fixture and the consent's project is not a test project: a fixture attestation is accepted only in test. Nothing was signed.
404objectNo such token.
409objectThe token was already submitted, or a concurrent submit won the race (nothing was created twice: the loser rolls back whole); or method: "verified_code" was claimed without a contact verification for this consumer in the last 24h, in which case nothing was created at all and the token is still pending.
410objectThe token expired. Start a new consent.

Response 201

FieldTypeRequiredDescription
callback_urlstring,nullyesThe callback_url given at creation, echoed so the caller can post the mandate on.
mandateobjectyesThe signed consumer mandate. Store it server-side with its signature; the agent presents both on every spend.
mandate_idstringyes—
signaturestringyesHMAC of the mandate's canonical payload, hex.
Example request
curl -X POST https://api.codespar.dev/v1/consents/{token}/submit \
  -H "Authorization: Bearer $CODESPAR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "consumer_id": "csm_0000000000000000",
       "rail": "pix-consent",
       "provider_token": "string",
       "display_label": "Example",
       "attestation": {
         "method": "partner_session",
         "asserted_at": 0,
         "reference": "string",
         "evidence": {
           "channel": "whatsapp",
           "message_id": "message_0000000000000000",
           "session_id": "ses_0000000000000000",
           "provider_ts": 0,
           "ip": "string",
           "user_agent": "string",
           "device_id_hash": "string",
           "geo": {
             "country": "string",
             "region": "string",
             "city": "string"
           },
           "contact": "string"
         }
       }
     }'
POST /v1/consents/{token}/submit HTTP/1.1
Host: api.codespar.dev
Authorization: Bearer $CODESPAR_API_KEY
Content-Type: application/json

{
  "consumer_id": "csm_0000000000000000",
  "rail": "pix-consent",
  "provider_token": "string",
  "display_label": "Example",
  "attestation": {
    "method": "partner_session",
    "asserted_at": 0,
    "reference": "string",
    "evidence": {
      "channel": "whatsapp",
      "message_id": "message_0000000000000000",
      "session_id": "ses_0000000000000000",
      "provider_ts": 0,
      "ip": "string",
      "user_agent": "string",
      "device_id_hash": "string",
      "geo": {
        "country": "string",
        "region": "string",
        "city": "string"
      },
      "contact": "string"
    }
  }
}
import os
import requests

res = requests.post(
    "https://api.codespar.dev/v1/consents/{token}/submit",
    headers={"Authorization": f"Bearer {os.environ['CODESPAR_API_KEY']}"},
    json={
      "consumer_id": "csm_0000000000000000",
      "rail": "pix-consent",
      "provider_token": "string",
      "display_label": "Example",
      "attestation": {
        "method": "partner_session",
        "asserted_at": 0,
        "reference": "string",
        "evidence": {
          "channel": "whatsapp",
          "message_id": "message_0000000000000000",
          "session_id": "ses_0000000000000000",
          "provider_ts": 0,
          "ip": "string",
          "user_agent": "string",
          "device_id_hash": "string",
          "geo": {
            "country": "string",
            "region": "string",
            "city": "string"
          },
          "contact": "string"
        }
      }
    },
)
res.raise_for_status()
data = res.json()
const res = await fetch("https://api.codespar.dev/v1/consents/{token}/submit", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CODESPAR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "consumer_id": "csm_0000000000000000",
    "rail": "pix-consent",
    "provider_token": "string",
    "display_label": "Example",
    "attestation": {
      "method": "partner_session",
      "asserted_at": 0,
      "reference": "string",
      "evidence": {
        "channel": "whatsapp",
        "message_id": "message_0000000000000000",
        "session_id": "ses_0000000000000000",
        "provider_ts": 0,
        "ip": "string",
        "user_agent": "string",
        "device_id_hash": "string",
        "geo": {
          "country": "string",
          "region": "string",
          "city": "string"
        },
        "contact": "string"
      }
    }
  }),
});

const data = await res.json();
const r = await cs.api.response("post", "/v1/consents/{token}/submit", {
  path: {
    token: "string"
  },
  body: {
    consumer_id: "csm_0000000000000000",
    rail: "pix-consent",
    provider_token: "string",
    display_label: "Example",
    attestation: {
      method: "partner_session",
      asserted_at: 0,
      reference: "string",
      evidence: {
        channel: "whatsapp",
        message_id: "message_0000000000000000",
        session_id: "ses_0000000000000000",
        provider_ts: 0,
        ip: "string",
        user_agent: "string",
        device_id_hash: "string",
        geo: {
          country: "string",
          region: "string",
          city: "string"
        },
        contact: "string"
      }
    }
  }
});
// r.status is one of the documented statuses (200, 403),
// each with its own body shape in r.data; nothing here throws on 403.
if (r.ok) {
  console.log(r.data);
}
Example response 201
application/json
{
  "mandate_id": "mandate_0000000000000000",
  "mandate": {},
  "signature": "string",
  "callback_url": "https://example.com/hook"
}
Consents | CodeSpar