Looking for the previous documentation?Go to v1.docs.pluggy.ai

Partner Support API

Open and track Pluggy support tickets from your own systems. A ticket created through this API is identical to one filed by hand in the support portal: same request type, same intake form, same SLA and the same queue. Your team keeps seeing everything in the portal as usual.

Machine-readable contract: /api/partners/docs (OpenAPI 3.0 — point your codegen at it).

Authentication

Send the API key your Pluggy contact issued you:

Authorization: Bearer pk_live_…
# or
X-Api-Key: pk_live_…

The key identifies your account — you never send a team or customer id, and a key can only open tickets for its own account. Treat it as a secret; if it leaks, ask us to revoke it and we issue a new one immediately.

Open a ticket

curl -X POST https://docs.pluggy.ai/api/partners/tickets \
  -H "Authorization: Bearer $PLUGGY_PARTNER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "summary": "Transação do cartão de crédito não sendo retornada",
    "description": "Item conectado e atualizado, fatura fechada em 05/08. As 3 últimas transações não aparecem na API. Afeta ~40 usuários desde 04/08.",
    "itemId": "8f2a1b3c-0000-4d4e-9a9a-111122223333",
    "product": "Credit Card",
    "problemType": "Transação do cartão de credito não sendo retornada",
    "accountType": "Pessoa Física",
    "externalId": "YOUR-1947",
    "priority": "P1 — Alto"
  }'

Response:

{
  "ticket": "SUP2-13826",
  "portalUrl": "https://pluggy.atlassian.net/servicedesk/customer/portal/1/SUP2-13826",
  "status": "Aberto",
  "created": true,
  "deduplicatedFrom": null,
  "attachmentsUploaded": 0,
  "derivedFromItem": { "connectorName": "Nubank", "isOpenFinance": true },
  "unmappedValues": []
}

Fields

FieldRequiredNotes
summaryyesShort title. Our workflow rewrites it as [YourOrg/Institution] problem (externalId), so keep it descriptive but don't rely on it.
descriptionyesWhat's wrong, since when, how many users are affected, how to reproduce. This is what the engineer reads first.
itemIdyesA Pluggy item where the problem reproduces. It must belong to your account, and its connector is what sets the institution — you don't send one. A ticket citing another account's item is refused before it is created.
productyesTransactions, Credit Card, Investments, Consent
problemTypeyesThe closest match from the enum. Picking well is what routes the ticket correctly.
externalIdstrongly recommendedYour own issue id. Makes the call idempotent — a retry returns the existing ticket instead of a duplicate — and shows in the ticket title so both sides can cross-reference.
accountTypenoPessoa Física, Pessoa Jurídica, Corretora or Outra. Tells us which kind of account to reproduce against.
accountIdnoThe Pluggy account the problem is on, when the item has more than one. Saves us a round trip asking which.
investmentIdnoSame, for investment problems: the specific investment that is wrong or missing.
prioritynoP0 — Crítico to P3 — Baixo. Your read of the impact — it is a signal for triage, not an SLA commitment.

Evidence and attachments

Screenshots, HAR files and logs make the difference between a ticket resolved in a day and one that bounces for a week. Send up to 5 files per ticket, 10 MB each, either as an HTTPS URL we fetch once (preferred — signed URLs with a short TTL are fine) or inline base64 for small files.

"attachments": [
  { "filename": "evidencia.har", "contentType": "application/json",
    "url": "https://files.example.com/signed/evidencia.har" },
  { "filename": "log.txt", "contentType": "text/plain",
    "base64": "TG9nIGNvbnRlbnQ=" }
]

Check status

curl https://docs.pluggy.ai/api/partners/tickets/SUP2-13826 \
  -H "Authorization: Bearer $PLUGGY_PARTNER_KEY"

Returns the current status and the SLA clocks. You can only read tickets that belong to your account.

Ticket events (webhooks)

Instead of polling for changes, give us an HTTPS endpoint and we will POST an event to it whenever one of your tickets moves. The API stays available for everything else — opening tickets, posting follow-ups, and reconciling when you want to be sure — so the webhook is the fast path, not the only one.

  1. Send us the HTTPS URL you want events on, through your usual Pluggy contact. A separate URL for your test environment is fine; tell us which is which.
  2. We reply with a signing secret (whsec_…). It is shown once and cannot be read back afterwards — store it where you keep your other credentials. If it is lost, ask us to rotate it.
  3. Verify the signature on every request, then answer any 2xx as soon as you have durably accepted the event. Queue it and process it asynchronously: we wait 8 seconds for a response, and anything slower counts as a failed delivery.

Five events, each fired from the same comparison against the ticket system that keeps our copy of your ticket correct — so an event can never claim a change that did not happen:

  • ticket.status_changedthe ticket moved to a different status. previous carries what it was.
  • ticket.resolvedthe ticket was resolved. Sent instead of a status change, not alongside it, so one real change is one event.
  • ticket.message_addedwe replied on the ticket. Carries the reply text.
  • ticket.priority_changedthe priority or SLA changed. The first priority we ever see on a ticket is not a change and sends nothing.
  • ticket.updatedthe title changed.
{
  "eventId": "0f2b6c4e-7a1d-4a2e-9c3f-5b8d1e0a7c44",
  "sequence": 1042,
  "event": "ticket.message_added",
  "occurredAt": "2026-09-18T13:42:07.912Z",
  "ticket": {
    "key": "SUP2-13826",
    "number": 418,
    "title": "Transação do cartão de crédito não sendo retornada",
    "state": "on_customer",
    "status": "On customer",
    "updatedAt": "2026-09-18T13:41:58.000Z",
    "portalUrl": "https://pluggy.atlassian.net/servicedesk/customer/portal/1/SUP2-13826"
  },
  "message": {
    "author": "Pluggy Support",
    "text": "Confirmamos com a instituição: as transações entram no próximo ciclo.",
    "sentAt": "2026-09-18T13:41:58.000Z"
  }
}

Every request carries `X-Pluggy-Signature: t=<unix seconds>,v1=<hmac hex>`. What is signed is the string `"{t}.{raw body}"` — the body exactly as it arrived on the wire, before any JSON parse, because re-serialising the parsed object changes the bytes and the signature will not match. The `t` is also the anti-replay token: reject anything older than five minutes.

import { createHmac, timingSafeEqual } from "node:crypto";

// `raw` is the body exactly as it arrived — parse it only after this passes.
export function verify(raw: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("=", 2) as [string, string]),
  );
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (!Number.isFinite(age) || age > 300) return false;   // anti-replay

  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${raw}`)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 ?? "");
  return a.length === b.length && timingSafeEqual(a, b);
}
  • Internal notes never leave. Our tickets carry internal notes alongside the replies you see. ticket.message_added only ever carries a message that is visible to you in the portal — never an internal one.
  • De-duplicate on `eventId`. It is stable across retries, and also sent as the X-Pluggy-Event-Id header. The same id arriving twice means the same change, not two changes.
  • Ordering is not guaranteed. sequence tells you which event was created first, so you can discard one you have already superseded — but it is not delivery order. Treat an event as a signal to read the ticket, not as the state itself.
  • We retry. Six attempts with backoff — 30s, 2m, 10m, 1h, then 6h — after which the delivery is marked failed and we can re-send it by hand with the same eventId. Those delays are when a retry becomes due, not a promise of when it lands.

Ground rules

  • One ticket per problem, not per end user. If the same connector bug affects 500 of your users, that is one ticket with the volume described — not 500 tickets. This is the single biggest factor in how fast you get answers.
  • Track and follow up GET /api/partners/tickets lists your tickets with the SLA due date and whether it was breached, paged by cursor (nextCursor in the response, send it back as cursor), and POST /api/partners/tickets/{key}/comments adds a public comment — the same thing your team would write in the portal, so support sees it in the conversation rather than in an internal note.
  • You no longer send the institution. We read it from the itemId, together with whether the connector is Open Finance. If the connector isn't one of the options in our intake form it is filed as “Outra” with the real name in the description — nothing is lost.
  • Before the ticket exists, we check the item Four things stop a ticket from being created — a deleted connection, no successful update in two weeks, only invalid-credential executions, or an Open Finance consent still waiting on the company's other admins. In all of them the fix is on the connection and support could only tell you the same. The response carries a code and a message written for your end user (see the error table below).
  • Always send externalId. Retries and double-fired automations are normal; idempotency is how they stop being duplicates.
  • Quota. Each key has a monthly ticket quota. Over it the API answers 429 with Retry-After. Back off and retry; don't hammer.
  • Use dryRun while integrating. Setting "dryRun": true validates your payload and returns what we would file, without creating anything or touching your quota.
  • What doesn't belong here: ongoing incidents (watch the status page), integration questions (docs assistant), and feature requests (talk to your Pluggy contact).

Errors

400Missing or invalid fields — the message names them.
401Missing, unknown or revoked key.
403The item does not belong to your account.
404No such item, or the connection was deleted (ITEM_NOT_FOUND). Also for an item created in the last few minutes — retry shortly.
409The item can't be investigated as it is: ITEM_TOO_OLD (re-sync the connection), ITEM_ONLY_HAS_INVALID_CREDENTIALS_EXECUTIONS (the end user has to reconnect) or OF_ITEM_RESOURCES_WITHOUT_MASTER_PERMISSIONS (the company's other admins still have to authorize).
413An attachment is over 10 MB.
429Monthly quota reached. Honour Retry-After.
5xxOur side. Retry with exponential backoff; the request is idempotent when you send externalId.