Requires
@beinfi/sdk@0.10.4. invoices.deliverable(), the invoiceId that checkout() returns and
presign with narrowed types landed in 0.10.4. On an earlier version the HTTP
routes already exist — call them directly. (0.10.3 was published with a stale
bundle and has none of this; do not use it.)One-time purchases only
The deliverable exists only on atype: "item" product with
pricingModel: "one_time". Any other combination:
subscription, not a download.
Attaching
Two ways. A product has one deliverable: saving again replaces the previous one.A link (the shortest path)
Good for Notion, Drive, a Vimeo video, your own members’ area:A file
Three steps, because the bytes go straight from your process to storage without passing through our API:objectKey: "uploaded object was not found; upload before saving". And it fills
in sizeBytes/contentType on its own when you omit them: uploading a PDF
without declaring anything returns contentType: "application/pdf" and the real
size.
The uploadUrl is good for 15 minutes. It only writes that one object, so
you can send it straight from your admin’s browser without passing your sk_
along.
If the environment has no storage,
presign answers 503. 503 storage_unconfigured means that environment has no object storage
configured — not that you got the call wrong. Use kind: "link" in that case:
the rest of the flow (grant, email, download) is identical either way.What happens when the payment confirms
In this order, with nothing from you:- Infi resolves the buyer and the deliverable for that invoice.
- Creates a grant: a unique token for that payment.
- Sends the email, subject
Seu acesso / Your download is ready, with the link.
UNIQUE (payment_id, deliverable_id)), not in luck.
A product with no deliverable is not an error: delivery simply does nothing.
Serving it yourself (recommended)
Do not depend on the buyer’s inbox. You have the link:200 { "grants": [] } — an empty
list, never a 404. That is deliberate: “not delivered yet” is a real state
you keep querying, and a 404 would be indistinguishable from a wrong id. So it
is safe to put in a polling loop, next to pay.waitForPaid.
Three reasons to prefer this path over the email:
- The email may simply not go out, and
emailSentAtis how you know. It stays null when sending did not happen — and the case that catches most people testing is an address that does not really exist. Measured:@example.com(a reserved domain, no provider delivers it) →emailSentAtnull forever; a real address → filled in about 8s. The grant is born in both cases, so the sale is deliverable even when the email was not. - A buyer with no email at all also generates a grant. Without this call the sale is half-delivered and you do not know it.
- The email can land in spam. Your thank-you page cannot.
The download link
302:
An unknown token answers
404. In the SDK, if you already hold the token,
infi.pay.downloadUrl(slug, token) assembles that URL.
The signed download URL is deliberately short (5 min) because it is generated on
every click: whoever shares the signed link shares something that expires.
What does not expire is the token — see below.
Replacing and removing
delete also erases the file from storage. Grants already issued stop
resolving — yesterday’s buyer loses access, so replace with save rather than
deleting when the idea is to publish a new version.
The whole flow
taxId is not optional for pix, and idempotencyKey is what keeps a double
click from charging twice — both are explained in
your first sale.
And if you refund?
The grant is a capability with no deadline: whoever holds the token downloads. A full refund switches it off — the link starts answering410 and the grant
comes back with revokedAt — and a partial refund does not. The whole rule,
including how to override it, is in refunds.
That covers the file only. If your product grants access to something else (a
members’ area, a Discord role, an API key), you are the one who cuts it:
subscribe to payment.refunded and read accessRevoked.